안녕하세요! 오늘 우리는 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 라이브러리의 일부로 이 클래스를 받았고 단순히 코드를 가져와서 다시 작성할 수 없다고 상상해 봅시다. 하지만 우리는 그의 행동을 바꿔야 합니다. 예를 들어, 우리는 객체에서 어떤 메소드가 호출될지 모르기 때문에 그 사람이 메소드를 호출할 때 먼저 “안녕하세요!”라고 말하길 원합니다. (무례한 사람을 좋아하는 사람은 없습니다.) 그러한 상황에서 우리는 어떻게 해야 합니까? 몇 가지 사항이 필요합니다.
-
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!"를 인쇄합니다).
- 원본 객체와 해당 프록시.
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
읽을 수 있지만 아직 너무 신경 쓰지 마세요. 프록시 객체를 생성하는 데 필요한 약간의 추가 정보를 얻고 있다는 점을 기억하세요. 네 번째 줄에서는 특수 클래스 와 해당 정적 메서드를 사용합니다 . Proxy
newProxyInstance()
//Создаем прокси нашего 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
그것이 패턴이라는 것을 말씀드리는 것을 깜빡했습니다 ! 축하합니다. 또 다른 것을 배우셨습니다! :) 그래서 보안과 관련된 대중적인 기술과 프레임워크에서 활발히 사용되고 있습니다. 프로그램에 로그인한 사용자만 실행할 수 있는 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으로 캐스트할 수 없습니다. 인터페이스가 있어야 한다는 것은 필수 요구 사항입니다. 프록시는 인터페이스 수준에서 작동합니다. 오늘은 여기까지입니다 :) 프록시 주제에 대한 추가 자료로 훌륭한 동영상 과 좋은 기사를 추천해 드릴 수 있습니다 . 자, 이제 몇 가지 문제를 해결하면 좋을 것 같습니다! :) 또 봐요!
GO TO FULL VERSION