java.lang.reflect.Proxy czyli prawie AOP za prawie darmo
Począwszy od Javy 1.3 programistom (głównie frameworków) została udostępniona niezwykle użyteczna klasa Proxy. Zasadniczo klasa ta pomaga w implementacji wzorca projektowego o tej samej nazwie, jednak skupię się na przykładzie użycia samej klasy, reszta będzie już oczywista.
Proxy jest swoistą warstwą pośredniczącą między obiektem docelowym a światem zewnętrznym. Wywołanie każdej metody obiektu docelowego "przechodzi" przez proxy, które ma pełen zestaw możliwości wpływania na to wywołanie (podejrzenie i zmiana parametrów, logowanie, a nawet całkowite zaniechanie wywołania właściwej metody).
Jeśli znacie metody z grupy java.util.Collections.synchronized*(), to koncepcyjnie zwracają one właśnie takie proxy, które opakowuje wszystkie wywołania metod docelowej kolekcji. Rolą proxy jest w tym przykładzie zapewnienie synchronizacji na poziomie każdej metody i oczywiście wywołanie właściwej metody. Podobnie możemy sobie wyobrazić proxy zabezpieczające kolekcję przed modyfikacją, które rzucałoby wyjątek przy próbie wywołania jakiejkolwiek metody zmieniającej tą kolekcję.
Koncepcja chyba jasna, przejdźmy do przykładu :-). Załóżmy, że w naszym projekcie używamy prostej klasy DAO
public interface Dao
void create(String record);
String restore(long id);
void update(String record);
void delete(String record);
}
i implementacja:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class StringDao implements Dao
private static final Log log = LogFactory.getLog(StringDao.class);
@Override
public void create(String record) {
log.info("create: " + record);
}
@Override
public void delete(String record) {
log.info("delete: " + record);
}
@Override
public String restore(long id) {
log.info("restore: " + id);
return "Record " + id;
}
@Override
public void update(String record) {
log.info("update: " + record);
}
}
Jeszcze prosty przykładowy program:
import org.apache.log4j.BasicConfigurator;
class Main {
public static void main(String[] args) {
BasicConfigurator.configure();
Dao
dao.create("Java");
dao.restore(0);
dao.update("Java 2");
dao.delete("Java");
}
}
i dla porządku jego wyjście:
0 [main] INFO StringDao - create: Java
0 [main] INFO StringDao - restore: 0
0 [main] INFO StringDao - update: Java 2
0 [main] INFO StringDao - delete: Java
Nihil novi sub sole, przejdźmy do meritum :-). Załóżmy, że z pewny sobie znanych powodów chcemy monitorować wywołania klasy StringDao. Oczywiście moglibyśmy napisać nową klasę również implementującą interfejs Dao
Oszczędzę czytelnikom kodu dla powyższego rozwiązania od razu przechodząc do klasy Proxy i tego, jak pomaga ona w rozwiązaniu następującego problemu. Po pierwsze tworzymy instancję klasy Proxy:
import java.lang.reflect.Proxy;
import org.apache.log4j.BasicConfigurator;
class Main {
public static void main(String[] args) {
BasicConfigurator.configure();
Dao
new LoggingHandler());
daoProxy.create("Java");
daoProxy.restore(0);
daoProxy.update("Java 2");
daoProxy.delete("Java");
}
}
Metoda przyjmuje 3 argumenty:
- classloader, parametr typu zamknij oczy i przejdź dalej.
- Lista interfejsów, które ma implementować wynikowe proxy. Póki co przyjmijmy tylko jeden.
- Obiekt, który będzie informowany o każdej próbie wywołania metod interfejsu Dao
.
Ważna jest jeszcze wartość zwracana przez tą fabrykę. Zwróćcie uwagę, że obiekt ten implementuje interfejs Dao
Zapewne pali was ciekawość, jak wygląda ten LoggingHandler. Proszę bardzo:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class LoggingHandler implements InvocationHandler {
private static final Log log = LogFactory.getLog(LoggingHandler.class);
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("Invoking: " + method.getName() + " with args: " + Arrays.deepToString(args));
return null;
}
}
Jaki będzie wynik tak zmodyfikowanego programu?
0 [main] INFO StringDao - create: Java
0 [main] INFO StringDao - restore: 0
0 [main] INFO StringDao - update: Java 2
0 [main] INFO StringDao - delete: Java
Pytanie: co zwróci każde z wywołań daoProxy?
Za każdym razem, gdy wywoływaliśmy metodę daoProxy, w rzeczywistości VM wołała invoke() dostarczonego obiektu LoggingHandler podając jej jako argument m.in. metodę, która miała zostać uruchomiona. A dlaczego nie ma już logów z samego StringDao? To bardzo proste, przecież instancja klasy LoggingHandler nie ma pojęcia o istnieniu takich obiektów! Jedyne co może zrobić to zrócić null udając, że to zwróciła właściwa metoda interfejsu Dao
Jak naprawić ten oczywisty błąd? Oczywiście wyposażyć LoggingHandler w instancję klasy docelowej. W tym momencie mamy już zaimplementowany modelowy przykład wzorca projektowego proxy. Co będzie robiło nasze proxy? A, powiedzmy że logowało czas wykonania każdej metody. Oto kompletny kod naszego rozwiązania:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class LoggingHandler implements InvocationHandler {
private static final Log log = LogFactory.getLog(LoggingHandler.class);
private Dao
public LoggingHandler(Dao
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
long startTime = System.currentTimeMillis();
Object ret = method.invoke(target, args);
log.info("Invocation time of " + method.getName() + ": " + (System.currentTimeMillis() - startTime) + "ms");
return ret;
}
}
import java.lang.reflect.Proxy;
import org.apache.log4j.BasicConfigurator;
class Main {
public static void main(String[] args) {
BasicConfigurator.configure();
Dao
Dao
new LoggingHandler(dao));
daoProxy.create("Java");
daoProxy.restore(0);
daoProxy.update("Java 2");
daoProxy.delete("Java");
}
}
Dla pewności oglądamy logi:
0 [main] INFO StringDao - create: Java
0 [main] INFO LoggingHandler - Invocation time of create: 0ms
0 [main] INFO StringDao - restore: 0
0 [main] INFO LoggingHandler - Invocation time of restore: 0ms
0 [main] INFO StringDao - update: Java 2
0 [main] INFO LoggingHandler - Invocation time of update: 0ms
0 [main] INFO StringDao - delete: Java
0 [main] INFO LoggingHandler - Invocation time of delete: 0ms
I na koniec: jeśli ktoś interesuje się programowaniem aspektowym, skojarzenie jest oczywiste... Jak zatem za pomocą klasy Proxy zaimplementować porady typu before, after, after returning, after throwing i around? Jaki typ porady został użyty tutaj?