CodeGym /Blog Java /Random-PL /Dynamiczne serwery proxy w Javie
Autor
Jesse Haniel
Lead Software Architect at Tribunal de Justiça da Paraíba

Dynamiczne serwery proxy w Javie

Opublikowano w grupie Random-PL
Cześć! Dzisiaj rozważymy dość ważny i interesujący temat: tworzenie dynamicznych klas proxy w Javie. Nie jest to bardzo proste, więc spróbujemy to rozgryźć na przykładach :) A więc najważniejsze pytanie: czym są dynamiczne proxy i do czego służą? Klasa pośrednicząca jest rodzajem „dodatku” do oryginalnej klasy, który pozwala nam zmienić zachowanie oryginalnej klasy, jeśli to konieczne. Co to znaczy „zmienić zachowanie” i jak to działa? Rozważ prosty przykład. Załóżmy, że mamy interfejs Person i prostą klasę Man , która implementuje ten interfejs

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.
}
Nasza klasa Man ma 3 metody: wprowadzenie, powiedz Wiek i powiedz Skąd. Wyobraź sobie, że dostaliśmy tę klasę jako część gotowej biblioteki JAR i nie możemy po prostu przepisać jej kodu. Ale musimy też zmienić jego zachowanie. Na przykład nie wiemy, jaka metoda może zostać wywołana na naszym obiekcie, ale chcemy, aby nasza osoba powiedziała „Cześć!” (nikt nie lubi kogoś, kto jest niegrzeczny), gdy wywoływana jest dowolna z metod. Dynamiczne serwery proxy — 2Co powinniśmy zrobić w tej sytuacji? Będziemy potrzebować kilku rzeczy:
  1. Program obsługi wywołań

Co to jest? InvocationHandler to specjalny interfejs, który pozwala nam przechwycić każde wywołanie metody do naszego obiektu i dodać dodatkowe zachowanie, którego potrzebujemy. Musimy stworzyć własny interceptor, czyli stworzyć klasę implementującą ten interfejs. To jest całkiem proste:

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;
   }
}
Musimy zaimplementować tylko jedną metodę interfejsu: invoke() . Nawiasem mówiąc, robi to, czego potrzebujemy: przechwytuje wszystkie wywołania metod do naszego obiektu i dodaje niezbędne zachowanie (wewnątrz metody invoke() wysyłamy „Cześć!” do konsoli).
  1. Oryginalny obiekt i jego proxy.
Tworzymy nasz oryginalny obiekt Man i do niego „dodatek” (proxy):

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());

   }
}
To nie wygląda na bardzo proste! Specjalnie dodałem komentarz dla każdej linii kodu. Przyjrzyjmy się bliżej temu, co się dzieje. W pierwszym wierszu po prostu tworzymy oryginalny obiekt, dla którego stworzymy proxy. Poniższe dwa wiersze mogą sprawić ci trudność:

 // 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();
Właściwie nie dzieje się tu nic szczególnego :) W czwartej linii używamy specjalnej klasy Proxy i jej statycznej metody newProxyInstance() :

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Ta metoda po prostu tworzy nasz obiekt proxy. Do metody przekazujemy informacje o oryginalnej klasie, którą otrzymaliśmy w ostatnim kroku (jej ClassLoader i listę jej interfejsów), a także utworzony wcześniej obiekt InvocationHandler . Najważniejszą rzeczą jest nie zapomnieć przekazać naszego oryginalnego obiektu arnold do procedury obsługi inwokacji, w przeciwnym razie nie będzie czego „obsługiwać” :) Co otrzymaliśmy? Mamy teraz obiekt proxy: proxyArnold . Może wywoływać dowolne metody interfejsu Person . Dlaczego? Ponieważ daliśmy mu listę wszystkich interfejsów tutaj:

// 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));
Teraz wie o wszystkich metodach interfejsu Person . Dodatkowo przekazaliśmy naszemu proxy obiekt PersonInvocationHandler skonfigurowany do pracy z obiektem arnold :

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Teraz, jeśli wywołamy dowolną metodę interfejsu Person na obiekcie proxy, nasz moduł obsługi przechwyci wywołanie i zamiast tego wykona własną metodę invoke() . Spróbujmy uruchomić metodę main() ! Wyjście konsoli:

Hi!
Doskonały! Widzimy, że zamiast oryginalnej metody Person.introduce() metoda invoke() naszej PersonInvocationHandler() nazywa się:

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

   System.out.println("Hi!");
   return null;
}
"Cześć!" jest wyświetlany na konsoli, ale nie jest to dokładnie to, czego chcieliśmy :/ Próbowaliśmy osiągnąć najpierw wyświetlenie „Cześć!” a następnie wywołać samą oryginalną metodę, innymi słowy wywołanie metody

proxyArnold.introduce(arnold.getName());
powinien wyświetlać „Cześć! Nazywam się Arnold”, a nie tylko „Cześć!” Jak możemy to osiągnąć? To nie jest skomplikowane: musimy tylko dać sobie trochę swobody z naszym handlerem i metodą invoke() :) Zwróć uwagę na to, jakie argumenty są przekazywane do tej metody:

public Object invoke(Object proxy, Method method, Object[] args)
Metoda invoke() ma dostęp do pierwotnie wywołanej metody i do wszystkich jej argumentów (metoda metody, argumenty obiektu[]). Innymi słowy, jeśli wywołamy metodę proxyArnold.introduce(arnold.getName()) tak, że wywołana zostanie metoda invoke() zamiast metody wprowadzenie() , to wewnątrz tej metody mamy dostęp do oryginalnej metody wprowadzenie() i jego argument! W rezultacie możemy to zrobić:

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);
   }
}
Teraz w metodzie invoke() dodaliśmy wywołanie oryginalnej metody. Jeśli teraz spróbujemy uruchomić kod z naszego poprzedniego przykładu:

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());
   }
}
wtedy zobaczymy, że teraz wszystko działa jak należy :) Wyjście konsoli:

Hi! My name is Arnold
Kiedy możesz tego potrzebować? Właściwie dość często. Wzorzec projektowy „dynamic proxy” jest aktywnie wykorzystywany w popularnych technologiach… A tak przy okazji, zapomniałem wspomnieć, że Dynamic Proxy to wzorzec projektowy! Gratulacje, nauczyłeś się jeszcze jednego! :) Dynamiczne serwery proxy — 3Na przykład jest aktywnie wykorzystywany w popularnych technologiach i frameworkach związanych z bezpieczeństwem. Wyobraź sobie, że masz 20 metod, które powinny być wykonywane tylko przez użytkowników zalogowanych do Twojego programu. Korzystając z technik, których się nauczyłeś, możesz łatwo dodać do tych 20 metod sprawdzanie, czy użytkownik wprowadził prawidłowe dane uwierzytelniające bez powielania kodu weryfikacyjnego w każdej metodzie. Lub załóżmy, że chcesz utworzyć dziennik, w którym będą rejestrowane wszystkie działania użytkownika. Można to również łatwo zrobić za pomocą serwera proxy. Nawet teraz możesz po prostu dodać kod do naszego powyższego przykładu, aby nazwa metody była wyświetlana po wywołaniu invoke() , a to dałoby super prosty log naszego programu :) Podsumowując, zwróć uwagę na jedno ważne ograniczenie. Obiekt proxy działa z interfejsami, a nie z klasami. Dla interfejsu tworzony jest serwer proxy. Spójrz na ten kod:

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
Tutaj tworzymy proxy specjalnie dla interfejsu Person . Jeśli spróbujemy stworzyć proxy dla klasy, czyli zmienić typ referencji i spróbować rzutować na klasę Man , to nie zadziała.

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

proxyArnold.introduce(arnold.getName());
Wyjątek w wątku „main” java.lang.ClassCastException: com.sun.proxy.$Proxy0 nie może być rzutowane na człowieka Posiadanie interfejsu jest bezwzględnym wymogiem. Serwery proxy współpracują z interfejsami. To wszystko na dziś :) A teraz dobrze byłoby rozwiązać kilka zadań! :) Do następnego razu!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION