2 grudnia 2007

Mechanizm dynamicznego proxy z Reflection API

Dzisiaj zaprezentuję mechanizm dynamicznego proxy (ang. dynamic proxy) udostępniany przez Reflection API, mechanizm wykorzystywany (między innymi) przez Spring’a do implementacji AOP (akr. Aspect-Oriented Programming). Generalnie, dynamiczne proxy umożliwia wywoływanie metod naszej klasy za pośrednictwem metod innej klasy. Pozwala to wykonać pewne akcje przed jak i po wywołaniu właściwej metody albo też ewentualnie zadecydować o nie wykonywaniu tejże. Ale gdyby to było tylko tyle to byłoby to zwykłe proxy. Dlaczego jest ono dynamiczne? Otóż dlatego, że klasa będąca klasą proxy jest generowana dynamicznie w czasie wykonania. Nie mam pomysłu jak zwięźle i trafnie opisać, o co dokładnie chodzi, więc od razu przechodzę do meritum. Załóżmy, że mamy następujący interfejs i implementującą go klasę:

public interface MyService {

public void myOperation();
}

public class MyServiceBean implements MyService {

public void myOperation () {
System.out.println("Message from myOperation()");
}
}

Aby to, co chcę finalnie zbudować wyglądało ładnie użyjemy Spring’a (choć można i bez niego). Poniżej konfiguracja ziarna dla klasy pokazanej powyżej, póki co nic ciekawego, ale będziemy modyfikować:

<bean id="myService" class="mypackage.MyServiceBean">
</bean>

A teraz zdefiniujmy interfejs z dwiema metodami – doBefore() i doAfter() – które to będą wywołane przed i po właściwym wywołaniu metody biznesowej. Dodatkowo, metoda doBefore() będzie zwracała rezultat typu logicznego (typu boolean) o tym znaczeniu, że metoda biznesowa będzie wywoływana tylko wtedy, gdy rezultat ten będzie miał wartość ‘true’. Oto on wraz z implementacją testową:

public interface ProxyAction {

public boolean doBefore();

public void doAfter();
}

public class InvokeAction implements ProxyAction {

public boolean doBefore() {
System.out.println("Message from doBefore()");

return true;
}

public void doAfter() {
System.out.println("Message from doAfter()");
}
}

Naszym celem jest oczywiście takie skonstruowanie kodu, aby to, jaka implementacja interfejsu ProxyAction będzie użyta zależało od konfiguracji Spring’a. To co było banalne już mamy, teraz przechodzimy do sedna.

W dzisiejszej zabawie chodzi o to, aby zastąpić definicję elementu zarządzanego Spring pokazaną powyżej taką definicją, aby zamiast instancji klasy implementującej metody biznesowe dostać instancje innej klasy, utworzonej dynamicznie dzięki mechanizmowi dynamicznych proxy z Reflection API. Taka instancja naturalnie będzie implementowała te same interfejsy co nasza klasa biznesowa. To jest właśnie gwóźdź programu. W tym celu musimy napisać klasę implementującą interfejs java.lang.reflect.InvocationHandler. Oto kod:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ProxyHandler implements java.lang.reflect.InvocationHandler {

private Object proxiedInstance;

private ProxyAction proxyAction;

protected ProxyHandler(Object obj, ProxyAction proxyAction) {
this.proxiedInstance = obj;
this.proxyAction = proxyAction;
}

public Object invoke(Object proxy, Method proxiedMethod, Object[] args)
throws Throwable {

Object result = null;

try {
if (proxyAction.doBefore())
result = proxiedMethod.invoke(proxiedInstance, args);

proxyAction.doAfter();
} catch (InvocationTargetException invocationExc) {
throw invocationExc.getTargetException();
}

return result;
}
}

Kolejnym elementem układanki jest klasa implementująca wzorzec fabryki i tworząca instancje odpowiednio skonfigurowanych proxy. Oto kod:

public class ProxiedServiceFactory {

private ProxyAction proxyAction;

public Object newInstance(Object proxiedInstance) {
return java.lang.reflect.Proxy.newProxyInstance(
proxiedInstance.getClass().getClassLoader(),
proxiedInstance.getClass().getInterfaces(),
new ProxyHandler(proxiedInstance, proxyAction));
}

public void setProxyAction(ProxyAction proxyAction) {
this.proxyAction = proxyAction;
}
}

Działa to tak, że fabryka buduje obiekt dynamicznego proxy na bazie instancji klasy implementującej logikę biznesową przekazanej do wywołania metody newInstance(). Budując wspomniany obiekt proxy przekazujemy jednocześnie instancję klasy implementującą interfejs java.lang.reflect.InvocationHandler. Metoda invoke() tej klasy będzie wywoływana zamiast metod biznesowych i to właśnie tam decydujemy, co tak naprawdę się stanie. Ja akurat postanowiłem, że wywołamy tam metody doBefore() i doAfter() na instancji implementującej interfejs MyService zdefiniowanej w momencie tworzenia fabryki. Aby spiąć wszystko elegancko do kupy należy użyć konfiguracji Spring pokazanej poniżej (w miejsce tej pokazanej na początku artykułu):

<bean id="proxiedServiceFactory" class="mypackage.ProxiedServiceFactory">
<property name="proxyAction">
<bean class="mypackage.InvokeAction" />
</property>
</bean>

<bean id="myService" factory-bean="proxiedServiceFactory" factory-method="newInstance">
<constructor-arg>
<bean class="mypackage.MyServiceBean" />
</constructor-arg>
</bean>

To tyle. Naturalnie, z dokładnością do możliwości użycia operatora ‘instanceof’, ziarna zarządzanego ‘myService’ używamy tak, jakby to była normalna instancja naszej klasy biznesowej. Podkreślę jeszcze tylko, bo być może nie jest to jasne, że to co pokazałem to jest rodzaj szkieletu aplikacji (ang. framework). Mam tu na myśli to, że całą tę skomplikowaną infrastrukturę implementujemy tylko raz, niezależnie od tego ile mamy klas biznesowych. W następnym artykule mam nadzieję pokazać przykład ciekawego wykorzystania pokazanego tu mechanizmu.

5 komentarzy:

Antoni Jakubiak pisze...

Nie napisałeś najważniejszego. Reflection API w języku java jest ograniczone - obiekt proxy możemy utworzyć tylko wtedy, gdy mamy zdefiniowany interfejs. Nie uda nam się na przykład utworzyć obiektu proxy dla encji JPA. A to bardzo poważne ograniczenie.

Ja polecam CGLIB. Cglib to biblioteka używana w między innymi projekcie Hibernate do dynamicznego tworzenia klas proxy oraz instancji jej obiektów.

Wygląda to mniej więcej tak:

MethodInterceptor methodInterceptor = new ....
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Xxx.class);
enhancer.setInterfaces(new Class[]});
enhancer.setCallback(methodInterceptor);
enhancer.create();

Mariusz Lipiński pisze...

Wielkie dzięki za komentarz,

już wiem gdzie sięgnąć gdy będę potrzebował proxy dla klas które nie mają interfejsów. W następnym artykule mam zamiar pokazać jak można wykonywać akcje w proxy na podstawie adnotacji umieszczonej na interfejsie. Tak czy inaczej zakładam konieczność istnienia interfejsu w scenariuszach które mnie interesują, czyli dla klas będących serwisami, tak więc to ograniczenie Reflection API mi nie przeszkadza.

Antoni Jakubiak pisze...

Ja akutat potrzebowałem encji stąd moje zainteresowanie cglib.

Widzę, że mamy podobne zainteresowania. Ja w moim projekcie open-source używam adnotacji do tłumaczeń. Liczę że za tydzień będzie już wersja beta.

http://code.google.com/p/jpa-translator/

Przy okazji, zapraszam zainteresowanych do projektu.

Mariusz Lipiński pisze...

Ciekawy projekt,

niestety póki co nie ma żadnego info jak to ma działać - czekam ze zniecierpliwieniem i proszę, nie popełnij błędu udostępniania ciekawego projektu bez choćby zdawkowej dokumentacji - moja diagnoza jest taka, że wiele ciekawych pomysłów umiera właśnie z powodu braku odpowiednich doców i tutotriali. Twoje info o postępach w pracach jest mile widziane na tym blogu - nie wachaj się wrzucić jakiegoś linka!

Antoni Jakubiak pisze...

Projekt piszę metodą eksperymentalną, na razie API nie jest stabilne.
Gdy tylko API uznanm za stabilne napiszę prosty tutorial. Jednak już dziś mogę podzielić się moimi przemyśleniami na ten temat:

http://www.jakubiak.eu/2007/12/tumaczenia-w-aplikacji-na-wiele-sposobw.html