CodeGym /Java Blog /무작위의 /Java의 동적 프록시
John Squirrels
레벨 41
San Francisco

Java의 동적 프록시

무작위의 그룹에 게시되었습니다
안녕! 오늘 우리는 Java에서 동적 프록시 클래스 생성이라는 다소 중요하고 흥미로운 주제를 고려할 것입니다. 그리 간단하지 않으므로 예제를 사용하여 알아내도록 노력하겠습니다 :) 가장 중요한 질문: 동적 프록시는 무엇이며 무엇을 위한 것입니까? 프록시 클래스는 원본 클래스 위에 추가되는 일종의 "추가 기능"으로, 필요한 경우 원본 클래스의 동작을 변경할 수 있습니다. "행동 변화"란 무엇을 의미하며 어떻게 작동합니까? 간단한 예를 고려하십시오. Person 인터페이스와 이 인터페이스를 구현하는 간단한 Man 클래스가 있다고 가정합니다.

public interface Person {

   public void introduce(String name);
  
   public void sayAge(int age);
  
   public void sayWhereFrom(String city, String country);
}

public class Man implements Person {

   private String name;
   private int age;
   private String city;
   private String country;

   public Man(String name, int age, String city, String country) {
       this.name = name;
       this.age = age;
       this.city = city;
       this.country = country;
   }

   @Override
   public void introduce(String name) {

       System.out.println("My name is " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("I am " + this.age + " years old");
   }

   @Override
   public void sayWhereFrom(String city, String country) {

       System.out.println("I'm from " + this.city + ", " + this.country);
   }

   // ...getters, setters, etc.
}
Man 클래스 에는 소개, sayAge 및 sayWhereFrom의 3가지 메서드가 있습니다. 기성품 JAR 라이브러리의 일부로 이 클래스를 얻었고 단순히 코드를 다시 작성할 수 없다고 상상해 보십시오. 그러나 우리는 또한 그것의 행동을 바꿀 필요가 있습니다. 예를 들어 객체에서 어떤 메서드가 호출될지 모르지만 상대방이 "안녕하세요!"라고 말하길 원합니다. (무례한 사람을 좋아하는 사람은 아무도 없습니다.) 메소드가 호출될 때. 동적 프록시 - 2이 상황에서 우리는 무엇을 해야 합니까? 몇 가지가 필요합니다.
  1. 호출 핸들러

이게 뭔가요? InvocationHandler는 개체에 대한 모든 메서드 호출을 가로채고 필요한 추가 동작을 추가할 수 있는 특수 인터페이스입니다. 우리는 우리 자신의 인터셉터를 만들어야 합니다. 즉, 이 인터페이스를 구현하는 클래스를 만들어야 합니다. 이것은 매우 간단합니다.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {
  
private Person person;

public PersonInvocationHandler(Person person) {
   this.person = person;
}

 @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

       System.out.println("Hi!");
       return null;
   }
}
하나의 인터페이스 메서드인 invoke() 만 구현하면 됩니다 . 그건 그렇고, 우리가 필요로 하는 것을 수행합니다: 객체에 대한 모든 메서드 호출을 가로채고 필요한 동작을 추가합니다(invoke () 메서드 내에서 콘솔에 "Hi!"를 출력합니다).
  1. 원래 객체와 해당 프록시.
우리는 원본 Man 객체와 이를 위한 "애드온"(프록시)을 생성합니다.

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());

   }
}
이것은 매우 간단해 보이지 않습니다! 각 코드 줄에 대해 구체적으로 주석을 추가했습니다. 무슨 일이 일어나고 있는지 자세히 살펴 보겠습니다. 첫 번째 줄에서는 프록시를 만들 원본 개체를 만듭니다. 다음 두 줄로 인해 문제가 발생할 수 있습니다.

 // Get the class loader from the original object
ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();
사실 여기서 특별한 일이 일어나는 것은 아닙니다. :) 네 번째 줄에서 특별한 Proxy 클래스와 정적 newProxyInstance() 메서드를 사용합니다.

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
이 방법은 프록시 객체를 생성합니다. 마지막 단계에서 받은 원래 클래스에 대한 정보( ClassLoader 및 해당 인터페이스 목록)와 이전에 만든 InvocationHandler 개체를 메서드에 전달합니다. 가장 중요한 것은 원래 arnold 객체를 호출 처리기에 전달하는 것을 잊지 않는 것입니다 . 그렇지 않으면 "처리"할 것이 없습니다 :) 우리는 무엇을 얻었습니까? 이제 프록시 객체인 proxyArnold 가 있습니다 . Person 인터페이스 의 모든 메서드를 호출할 수 있습니다 . 왜? 여기에 모든 인터페이스 목록을 제공했기 때문입니다.

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
이제 Person 인터페이스 의 모든 메서드에 대해 알고 있습니다 . 또한 arnold 객체 와 함께 작동하도록 구성된 PersonInvocationHandler 객체를 프록시에 전달했습니다 .

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
이제 프록시 개체에서 Person 인터페이스 의 메서드를 호출하면 핸들러가 호출을 가로채 자체 invoke() 메서드를 대신 실행합니다. main() 메서드를 실행해 봅시다 ! 콘솔 출력:

Hi!
훌륭한! 원래 Person.introduce() 메서드 대신 PersonInvocationHandler() 의 invoke() 메서드가 호출되는 것을 볼 수 있습니다.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Hi!");
   return null;
}
"안녕!" 가 콘솔에 표시되지만 이것은 정확히 우리가 원하는 동작이 아닙니다./ 우리가 달성하려고 했던 것은 먼저 "Hi!"를 표시하는 것입니다. 그런 다음 원래 메소드 자체를 호출합니다. 즉, 메소드 호출은

proxyArnold.introduce(arnold.getName());
단순히 "Hi!"가 아니라 "Hi! My name is Arnold"를 표시해야 합니다. 이것을 어떻게 달성할 수 있습니까? 복잡하지 않습니다: 핸들러와 invoke() 메서드 를 사용하여 약간의 자유를 얻으면 됩니다 :) 이 메서드에 어떤 인수가 전달되는지 주의하십시오.

public Object invoke(Object proxy, Method method, Object[] args)
invoke () 메서드는 원래 호출된 메서드와 모든 인수(Method 메서드, Object[] args)에 액세스할 수 있습니다. 즉, proxyArnold.introduce(arnold.getName()) 메서드를 호출하여 invoke() 메서드 대신에 invoke () 메서드를 호출하면 이 메서드 내에서 원래의 insert() 메서드 에 액세스할 수 있습니다. 그리고 그 주장! 결과적으로 다음과 같이 할 수 있습니다.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class PersonInvocationHandler implements InvocationHandler {

   private Person person;

   public PersonInvocationHandler(Person person) {

       this.person = person;
   }

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("Hi!");
       return method.invoke(person, args);
   }
}
이제 invoke() 메서드에서 원래 메서드에 대한 호출을 추가했습니다. 이제 이전 예제의 코드를 실행하려고 하면 다음과 같습니다.

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());
   }
}
이제 모든 것이 정상적으로 작동하는 것을 볼 수 있습니다 :) 콘솔 출력:

Hi! My name is Arnold
언제 이것이 필요할 수 있습니까? 사실 꽤 자주. "동적 프록시" 디자인 패턴은 인기 있는 기술에서 활발히 사용됩니다... 아, 그런데 동적 프록시가 디자인 패턴이라는 것을 언급하는 것을 잊었습니다! 축하합니다. 하나 더 배웠습니다! :) 동적 프록시 - 3예를 들어, 보안과 관련된 대중적인 기술과 프레임워크에서 활발히 사용되고 있습니다. 프로그램에 로그인한 사용자만 실행해야 하는 20개의 메서드가 있다고 상상해 보십시오. 배운 기술을 사용하여 이러한 20가지 방법에 각 방법에서 확인 코드를 복제하지 않고 사용자가 유효한 자격 증명을 입력했는지 확인하는 검사를 쉽게 추가할 수 있습니다. 또는 모든 사용자 작업이 기록되는 로그를 생성한다고 가정합니다. 이것은 또한 프록시를 사용하여 쉽게 수행할 수 있습니다. 지금도 위의 예제에 코드를 추가하여 invoke() 를 호출할 때 메서드 이름이 표시되도록 할 수 있으며 그러면 프로그램의 매우 간단한 로그가 생성됩니다. :) 결론적으로 한 가지 중요한 제한 사항에 주의하십시오. 프록시 개체는 클래스가 아닌 인터페이스와 함께 작동합니다. 인터페이스에 대한 프록시가 생성됩니다. 이 코드를 살펴보십시오.

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
여기에서 우리는 특히 Person 인터페이스 를 위한 프록시를 생성합니다 . 클래스에 대한 프록시를 만들려고 하면, 즉 참조 유형을 변경하고 Man 클래스 로 캐스트하려고 하면 작동하지 않습니다.

Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

proxyArnold.introduce(arnold.getName());
"main" 스레드의 예외 java.lang.ClassCastException: com.sun.proxy.$Proxy0은 Man으로 캐스트할 수 없습니다. 인터페이스가 있어야 합니다. 프록시는 인터페이스와 함께 작동합니다. 오늘은 여기까지입니다 :) 자, 이제 몇 가지 과제를 해결하면 좋을 것 같습니다! :) 다음 시간까지!
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION