JavaRush /Java Blog /Random-KO /Java의 동적 프록시

Java의 동적 프록시

Random-KO 그룹에 게시되었습니다
안녕하세요! 오늘 우리는 Java로 동적 프록시 클래스를 생성하는 다소 중요하고 흥미로운 주제를 살펴볼 것입니다. 너무 간단하지 않으니 예시를 통해 알아보도록 하겠습니다. :) 그럼 가장 중요한 질문은 동적 프록시란 무엇이며 그 용도는 무엇입니까? 프록시 클래스는 원래 클래스에 대한 일종의 "상위 구조"로, 필요한 경우 동작을 변경할 수 있습니다. 행동을 변화시킨다는 것은 무엇을 의미하며 어떻게 작동합니까? 간단한 예를 살펴보겠습니다. 인터페이스 와 이 인터페이스를 구현하는 Person간단한 클래스가 있다고 가정해 보겠습니다.Man
public interface Person {

   public void introduce(String name);

   public void sayAge(int age);

   public void sayFrom(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("Меня зовут " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("Мне " + this.age + " years");
   }

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

       System.out.println("Я из города " + this.city + ", " + this.country);
   }

   //..геттеры, сеттеры, и т.д.
}
우리 수업에는 Man자기 소개, 나이 말하기, 출신지 말하기의 3가지 방법이 있습니다. 우리가 이미 만들어진 JAR 라이브러리의 일부로 이 클래스를 받았고 단순히 코드를 가져와서 다시 작성할 수 없다고 상상해 봅시다. 하지만 우리는 그의 행동을 바꿔야 합니다. 예를 들어, 우리는 객체에서 어떤 메소드가 호출될지 모르기 때문에 그 사람이 메소드를 호출할 때 먼저 “안녕하세요!”라고 말하길 원합니다. (무례한 사람을 좋아하는 사람은 없습니다.) 동적 프록시 - 1그러한 상황에서 우리는 어떻게 해야 합니까? 몇 가지 사항이 필요합니다.
  1. InvocationHandler

그것은 무엇입니까? 문자 그대로 "호출 인터셉터"로 번역될 수 있습니다. 이것은 그 목적을 매우 정확하게 설명합니다. 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("Hello!");
       return null;
   }
}
우리는 인터페이스 메소드 하나만 구현하면 됩니다 invoke(). 실제로 이는 우리가 필요로 하는 작업을 수행합니다. 즉, 개체에 대한 모든 메서드 호출을 가로채고 필요한 동작을 추가합니다(여기서는 invoke()메서드 내부의 콘솔에 "Hello!"를 인쇄합니다).
  1. 원본 객체와 해당 프록시.
Man원본 개체 와 이에 대한 "상부 구조"(프록시)를 만들어 보겠습니다 .
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       //Создаем оригинальный an object
       Man vasia = new Man("Vasya", 30, "Санкт-Петербург", "Россия");

       //Получаем загрузчик класса у оригинального an object
       ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

       //Получаем все интерфейсы, которые реализует оригинальный an object
       Class[] interfaces = vasia.getClass().getInterfaces();

       //Создаем прокси нашего an object vasia
       Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

       //Вызываем у прокси an object один из методов нашего оригинального an object
       proxyVasia.introduce(vasia.getName());

   }
}
별로 단순해 보이지는 않네요! 나는 각 코드 줄에 대해 구체적으로 설명을 작성했습니다. 여기서 무슨 일이 일어나고 있는지 자세히 살펴보겠습니다.

첫 번째 줄에서는 프록시를 생성할 원본 개체를 만듭니다. 다음 두 줄은 혼란을 야기할 수 있습니다.
//Получаем загрузчик класса у оригинального an object
ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();
하지만 여기서는 실제로 특별한 일이 없습니다. :) 프록시를 생성하려면 ClassLoader원본 개체의 클래스 로더와 원본 클래스(예: Man)가 구현하는 모든 인터페이스 목록이 필요합니다. 그것이 무엇인지 모른다면 JVM에 클래스를 로드하는 방법에 대한 이 기사나 Habré에 대한 이 기사를ClassLoader 읽을 수 있지만 아직 너무 신경 쓰지 마세요. 프록시 객체를 생성하는 데 필요한 약간의 추가 정보를 얻고 있다는 점을 기억하세요. 네 번째 줄에서는 특수 클래스 와 해당 정적 메서드를 사용합니다 . ProxynewProxyInstance()
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
이 메소드는 프록시 객체를 생성합니다. 이전 단계에서 받은 원래 클래스(이 클래스 ClassLoader와 해당 인터페이스 목록)에 대한 정보와 이전에 생성한 인터셉터의 개체인 InvocationHandler'a.'에 대한 정보를 메서드에 전달합니다. 가장 중요한 것은 원래 객체를 인터셉터에 전달하는 것을 잊지 마세요 vasia. 그렇지 않으면 "인터셉트"할 것이 아무것도 없을 것입니다 :) 우리는 결국 어떻게 되었나요? 이제 프록시 객체가 생겼습니다 vasiaProxy. 모든 인터페이스 메소드를Person 호출할 수 있습니다 . 왜? 모든 인터페이스 목록을 여기에 전달했기 때문입니다.
//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();

//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
이제 그는 모든 인터페이스 방법을 "인식"합니다 Person. 또한 PersonInvocationHandler객체와 함께 작동하도록 구성된 객체를 프록시에 전달했습니다 vasia.
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
이제 프록시 객체에서 인터페이스 메서드를 호출하면 Person인터셉터는 이 호출을 "잡아서" 대신 자체 메서드를 실행합니다 invoke(). 메소드를 실행해 봅시다 main()! 콘솔 출력: 안녕하세요! 엄청난! 실제 메서드 대신 다음 메서드가 호출되는 것을 볼 수 Person.introduce()있습니다 . invoke()PersonInvocationHandler()
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Hello!");
   return null;
}
그리고 콘솔에 "Hello!"가 표시되었습니다. 하지만 이는 정확히 우리가 원하는 동작이 아닙니다./ 우리 생각에 따르면 "Hello!"가 먼저 표시되어야 하고 그 다음 호출하는 메서드 자체가 작동해야 합니다. 즉, 이 메서드 호출은 다음과 같습니다.
proxyVasia.introduce(vasia.getName());
콘솔에 “Hello! 내 이름은 Vasya입니다." 단지 "안녕하세요!"가 아닙니다. 어떻게 이를 달성할 수 있나요? 복잡한 것은 없습니다. 인터셉터와 메소드를 조금만 수정하면 됩니다. invoke():) 이 메소드에 전달되는 인수에 주의하세요.
public Object invoke(Object proxy, Method method, Object[] args)
메서드는 invoke()대신 호출되는 메서드와 모든 인수 (메서드 메서드, Object[] args)에 액세스할 수 있습니다. 즉, 메소드를 호출 proxyVasia.introduce(vasia.getName())하고 메소드 대신 introduce()메소드가 호출되면 invoke()해당 메소드 내에서 원래 메소드 introduce()와 해당 인수에 모두 액세스할 수 있습니다. 결과적으로 우리는 다음과 같은 작업을 수행할 수 있습니다.
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("Hello!");
       return method.invoke(person, args);
   }
}
invoke()이제 원래 메서드에 대한 호출을 메서드에 추가했습니다 . 이제 이전 예제의 코드를 실행해 보면 다음과 같습니다.
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       //Создаем оригинальный an object
       Man vasia = new Man("Vasya", 30, "Санкт-Петербург", "Россия");

       //Получаем загрузчик класса у оригинального an object
       ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

       //Получаем все интерфейсы, которые реализует оригинальный an object
       Class[] interfaces = vasia.getClass().getInterfaces();

       //Создаем прокси нашего an object vasia
       Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

       //Вызываем у прокси an object один из методов нашего оригинального an object
       proxyVasia.introduce(vasia.getName());
   }
}
그러면 이제 모든 것이 정상적으로 작동하는 것을 볼 수 있습니다. :) 콘솔 출력: 안녕하세요! 제 이름은 Vasya입니다. 어디에 필요한가요? 실제로 많은 곳이 있습니다. "동적 프록시" 디자인 패턴은 대중적인 기술에서 활발히 사용되고 있습니다... 그런데, Dynamic Proxy그것이 패턴이라는 것을 말씀드리는 것을 깜빡했습니다 ! 축하합니다. 또 다른 것을 배우셨습니다! :) 동적 프록시 - 2그래서 보안과 관련된 대중적인 기술과 프레임워크에서 활발히 사용되고 있습니다. 프로그램에 로그인한 사용자만 실행할 수 있는 20개의 메서드가 있다고 상상해 보세요. 배운 기술을 사용하면 각 방법에서 별도로 확인 코드를 복제하지 않고도 사용자가 로그인 및 비밀번호를 입력했는지 확인하는 검사를 20가지 방법에 쉽게 추가할 수 있습니다. 또는 예를 들어 모든 사용자 작업이 기록되는 로그를 생성하려는 경우 프록시를 사용하면 이 작업도 쉽게 수행할 수 있습니다. 지금도 가능합니다. 호출 시 메서드 이름이 콘솔에 표시되도록 예제에 코드를 추가하기만 하면 invoke()프로그램의 간단한 로그를 얻을 수 있습니다. :) 강의가 끝나면 중요한 한 가지에 주의하세요. 제한 . 프록시 객체 생성은 클래스 수준이 아닌 인터페이스 수준에서 발생합니다. 인터페이스에 대한 프록시가 생성됩니다. 이 코드를 살펴보세요:
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
여기서는 인터페이스를 위한 프록시를 특별히 생성합니다 Person. 클래스에 대한 프록시를 만들려고 하면, 즉 링크 유형을 변경하고 클래스로 캐스팅하려고 하면 Man아무 것도 작동하지 않습니다.
Man proxyVasia = (Man) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

proxyVasia.introduce(vasia.getName());
스레드 "main"의 예외 java.lang.ClassCastException: com.sun.proxy.$Proxy0을 Man으로 캐스트할 수 없습니다. 인터페이스가 있어야 한다는 것은 필수 요구 사항입니다. 프록시는 인터페이스 수준에서 작동합니다. 오늘은 여기까지입니다 :) 프록시 주제에 대한 추가 자료로 훌륭한 동영상 과 좋은 기사를 추천해 드릴 수 있습니다 . 자, 이제 몇 가지 문제를 해결하면 좋을 것 같습니다! :) 또 봐요!
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION