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
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 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 Man
ma 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). Co powinniśmy zrobić w takiej sytuacji? Będziemy potrzebować kilku rzeczy:
-
InvocationHandler
InvocationHandler
to 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).
- Oryginalny obiekt i jego proxy.
Man
dla 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 ClassLoader
modułu ładującego klasy oryginalnego obiektu i listy wszystkich interfejsów, które Man
implementuje 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 Proxy
i 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 ClassLoader
oraz 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 PersonInvocationHandler
skonfigurowany 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 Proxy
to jest wzorzec ! Gratulacje, nauczyłeś się kolejnego! :) Jest 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!
GO TO FULL VERSION