JavaRush /Blog Java /Random-PL /Dynamiczne proxy w Javie

Dynamiczne proxy w Javie

Opublikowano w grupie Random-PL
Cześć! Dzisiaj przyjrzymy się dość ważnemu i interesującemu tematowi - tworzeniu dynamicznych klas proxy w Javie. Nie jest to zbyt proste, więc spróbujmy to zrozumieć na przykładach :) Zatem najważniejsze pytanie: czym są dynamiczne proxy i do czego służą? Klasa zastępcza to swego rodzaju „nadbudowa” nad klasą pierwotną, która pozwala w razie potrzeby zmienić jej zachowanie. Co to znaczy zmienić zachowanie i jak to działa? Spójrzmy na prosty przykład. Załóżmy, że mamy interfejs Personi prostą klasę Man, która implementuje ten interfejs
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 + „lata”);
   }

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

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

   //..геттеры, сеттеры, и т.д.
}
Nasza klasa Manma 3 metody: przedstaw się, podaj swój wiek i powiedz, skąd jesteś. Wyobraźmy sobie, że otrzymaliśmy tę klasę jako część gotowej biblioteki JAR i nie możemy po prostu pobrać i przepisać jej kodu. Musimy jednak zmienić jego zachowanie. Na przykład nie wiemy, która metoda zostanie wywołana na naszym obiekcie, dlatego chcemy, aby osoba wywołująca którąś z nich najpierw powiedziała „Hello!”. (nikt nie lubi kogoś, kto jest niegrzeczny). Dynamiczne proxy - 1Co powinniśmy zrobić w takiej sytuacji? Będziemy potrzebować kilku rzeczy:
  1. InvocationHandler

Co to jest? Można to dosłownie przetłumaczyć jako „przechwytywacz połączeń”. To dość dokładnie opisuje jego cel. InvocationHandlerto specjalny interfejs, który pozwala nam przechwytywać dowolne wywołania metod naszego obiektu i dodawać potrzebne nam dodatkowe zachowania. Musimy stworzyć własny przechwytywacz - czyli stworzyć klasę i zaimplementować ten interfejs. To 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("Cześć!");
       return null;
   }
}
Musimy zaimplementować tylko jedną metodę interfejsu - invoke(). Właściwie robi to, czego potrzebujemy - przechwytuje wszystkie wywołania metod naszego obiektu i dodaje niezbędne zachowanie (tutaj wypisujemy invoke()„Hello!” na konsoli wewnątrz metody).
  1. Oryginalny obiekt i jego proxy.
Stwórzmy Mandla niego oryginalny obiekt i „nadbudowę” (proxy):
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

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

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

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

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

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

   }
}
Nie wygląda to bardzo prosto! Specjalnie napisałem komentarz do każdej linii kodu: przyjrzyjmy się bliżej temu, co się tam dzieje.

W pierwszej linii po prostu tworzymy oryginalny obiekt, dla którego utworzymy proxy. Poniższe dwa wiersze mogą wprowadzić Cię w błąd:
//Получаем загрузчик класса у оригинального obiektа
ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();

//Получаем все интерфейсы, которые реализует оригинальный obiekt
Class[] interfaces = vasia.getClass().getInterfaces();
Ale tak naprawdę nie dzieje się tu nic specjalnego :) Aby utworzyć proxy, potrzebujemy ClassLoadermodułu ładującego klasy oryginalnego obiektu i listy wszystkich interfejsów, które Manimplementuje nasza oryginalna klasa (tj. ). Jeśli nie wiesz co to jest ClassLoader, możesz przeczytać ten artykuł o ładowaniu klas do JVM lub ten na Habré , ale nie przejmuj się tym jeszcze zbytnio. Pamiętaj tylko, że otrzymujemy trochę dodatkowych informacji, które będą nam potrzebne do utworzenia obiektu proxy. W czwartej linii używamy specjalnej klasy Proxyi jej metody statycznej newProxyInstance():
//Создаем прокси нашего obiektа vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
Ta metoda po prostu tworzy nasz obiekt proxy. Do metody przekazujemy informację o oryginalnej klasie, którą otrzymaliśmy w poprzednim kroku (ona ClassLoaderoraz listę jej interfejsów), a także obiekt utworzonego wcześniej przechwytywacza - InvocationHandler'a. Najważniejsze to nie zapomnieć przekazać naszego oryginalnego obiektu przechwytywaczowi vasia, w przeciwnym razie nie będzie on miał czego „przechwycić” :) Co nam wyszło? Mamy teraz obiekt proxy vasiaProxy. Może wywoływać dowolne metody interfejsuPerson . Dlaczego? Ponieważ przekazaliśmy mu listę wszystkich interfejsów - tutaj:
//Получаем все интерфейсы, которые реализует оригинальный obiekt
Class[] interfaces = vasia.getClass().getInterfaces();

//Создаем прокси нашего obiektа vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
Teraz jest „świadomy” wszystkich metod interfejsu Person. Dodatkowo przekazaliśmy naszemu proxy obiekt PersonInvocationHandlerskonfigurowany do pracy z obiektem vasia:
//Создаем прокси нашего obiektа vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
Teraz, jeśli wywołamy jakąkolwiek metodę interfejsu na obiekcie proxy Person, nasz przechwytywacz „złapie” to wywołanie i zamiast tego wykona własną metodę invoke(). Spróbujmy uruchomić metodę main()! Wyjście konsoli: Witam! Świetnie! Widzimy, że zamiast prawdziwej metody nasza Person.introduce()metoda nazywa się : invoke()PersonInvocationHandler()
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Cześć!");
   return null;
}
A konsola wyświetliła „Hello!” Jednak nie jest to dokładnie takie zachowanie jakie chcieliśmy uzyskać :/ Według naszego pomysłu najpierw powinno wyświetlić się „Hello!”, a dopiero potem powinna zadziałać sama metoda, którą wywołujemy. Innymi słowy, wywołanie tej metody:
proxyVasia.introduce(vasia.getName());
powinien wyświetlić na konsoli komunikat „Hello! Mam na imię Wasia” i nie tylko „Witam!” Jak możemy to osiągnąć? Nic skomplikowanego: wystarczy trochę majstrować przy naszym przechwytywaczu i metodzie invoke():) Zwróć uwagę jakie argumenty są przekazywane tej metodzie:
public Object invoke(Object proxy, Method method, Object[] args)
Metoda invoke()ma dostęp do metody, którą jest wywoływana, oraz do wszystkich jej argumentów (metoda metody, argumenty obiektu []). Innymi słowy, jeśli wywołamy metodę proxyVasia.introduce(vasia.getName())i zamiast metody introduce()zostanie wywołana metoda invoke(), wewnątrz tej metody będziemy mieli dostęp zarówno do oryginalnej metody, introduce()jak i jej argumentu! W rezultacie możemy zrobić coś takiego:
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("Cześć!");
       return method.invoke(person, args);
   }
}
Teraz do metody dodaliśmy invoke()wywołanie oryginalnej metody. Jeśli teraz spróbujemy uruchomić kod z poprzedniego przykładu:
import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

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

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

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

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

       //Вызываем у прокси obiektа один из методов нашего оригинального obiektа
       proxyVasia.introduce(vasia.getName());
   }
}
wtedy zobaczymy, że teraz wszystko działa jak należy :) Wyjście z konsoli: Witam! Mam na imię Vasya. Gdzie możesz tego potrzebować? Właściwie w wielu miejscach. Wzorzec projektowy „dynamic proxy” jest aktywnie wykorzystywany w popularnych technologiach… a swoją drogą zapomniałem powiedzieć, że Dynamic Proxyto jest wzorzec ! Gratulacje, nauczyłeś się kolejnego! :) Dynamiczne proxy - 2Jest więc aktywnie wykorzystywany w popularnych technologiach i frameworkach związanych z bezpieczeństwem. Wyobraź sobie, że masz 20 metod, które mogą wykonać tylko zalogowani użytkownicy Twojego programu. Korzystając z poznanych technik, możesz łatwo dodać do tych 20 metod sprawdzenie, czy użytkownik wprowadził login i hasło, bez konieczności powielania kodu weryfikacyjnego osobno w każdej metodzie. Lub, na przykład, jeśli 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. Możesz już nawet teraz: wystarczy dodać kod do przykładu tak, aby przy wywołaniu wyświetlała się w konsoli nazwa metody invoke(), a otrzymasz prosty log naszego programu :) Na koniec wykładu zwróć uwagę na jedną ważną ograniczenie . Tworzenie obiektu proxy odbywa się na poziomie interfejsu, a nie na poziomie klasy. Dla interfejsu tworzony jest serwer proxy. Spójrz na ten kod:
//Создаем прокси нашего obiektа vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
Tutaj tworzymy serwer proxy specjalnie dla interfejsu Person. Jeśli spróbujemy utworzyć proxy dla klasy, czyli zmienimy typ łącza i spróbujemy rzutować na klasę Man, nic nie zadziała.
Man proxyVasia = (Man) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));

proxyVasia.introduce(vasia.getName());
Wyjątek w wątku „main” java.lang.ClassCastException: com.sun.proxy.$Proxy0 nie może zostać rzutowany na Mana. Posiadanie interfejsu jest wymogiem. Proxy działa na poziomie interfejsu. To tyle na dziś :) Jako dodatkowy materiał na temat proxy mogę polecić Państwu świetny film i jednocześnie dobry artykuł . Cóż, teraz byłoby miło rozwiązać kilka problemów! :) Do zobaczenia!
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION