środa, 12 listopada 2008

PropertyChangeSupport - wywołanie za pomocą Spring AOP

W poprzednim tekście opisałem jak poradzić sobie z problemem opisanym na pl.comp.lang.java w poście zatytułowanym JavaBeans - zła koncepcja używając czystego AspectJ. Dziś pokażę jak zrobić to samo wykorzystując Spring AOP (przypomnę - chodzi o notyfikację obiektu PropertyChangeSupport przy każdym wywołaniu settera na JavaBean'ie).

Najważniejszym elementem rozwiązania jest interceptor, który przechwytuje wywołania setterów:

package pl.kadamczyk.springpropertychange.interceptors;

import java.lang.reflect.Field;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import pl.kadamczyk.springpropertychange.model.BaseModelObject;

public class SetterMethodInterceptor implements MethodInterceptor {

/**
* Otacza faktyczne wywołanie metody
*/
public Object invoke(final MethodInvocation method) throws Throwable {
String jointPoint = method.getMethod().getName();

// wyznaczamy nazwe pola na podstawie nazwy wywolanego settera
String fieldName = jointPoint.replaceFirst("set", "");
fieldName = Character.toLowerCase(fieldName.charAt(0))
+ fieldName.substring(1);

try {
Object target = method.getThis();

Class clazz = target.getClass();
// pobieramy pole klasy o wyznaczonej wczesniej nazwie
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);

// pobieramy stara wartosc pola
Object oldValue = field.get(target);
// zezwalamy na wywolanie settera
Object result = method.proceed();
if (method.getArguments().length == 1) {
triggerPropertyChange(target, method.getArguments()[0],
oldValue, fieldName);
}

return result;
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}

/**
* Sprawdza, czy metoda została wykonana na obiekcie klasy dziedziczącej
* po BaseModelObject, a jeśli tak, wywołuje na nim firePropertyChange
*/
private void triggerPropertyChange(final Object target,
final Object newVal, final Object oldValue, final String fieldName) {
System.out.println("MethodInterceptor: Zmieniamy wartosc pola "
+ fieldName);

if (target instanceof BaseModelObject) {
// wywolujemy metode z klasy bazowej
((BaseModelObject) target).firePropertyChange(fieldName, oldValue,
newVal);
}
}
}

Jest to częściowo zmieniona wersja aspektu z poprzedniego posta, dostosowana do Springowego API.

Drugim krokiem jest odpowiednie skonfigurowanie interceptora:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">

<aop:aspectj-autoproxy />

<bean id="someModelObject"
class="pl.kadamczyk.springpropertychange.model.SomeModelObject"
scope="prototype" />

<bean name="setterInterceptor"
class="pl.kadamczyk.springpropertychange.interceptors.SetterMethodInterceptor" />

<bean name="pointcut.setterAdvisor"
class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
<property name="advice" ref="setterInterceptor" />
<property name="mappedName" value="set*" />
</bean>

</beans>
Na uwagę zasługuje bean pointcut.setterAdvisor który odpowiada za powiązanie interceptora z pointcut'ami w których ma być zaaplikowany.

W odróżnieniu od pierwszego rozwiązania w którym zastosowałem czysty AspectJ oraz compile-time-weaving, tutaj aplikowanie porady wykonywane jest w trakcie działania.
Konsekwencją tego jest zmiana sposobu korzystania z klas modelu. Aby Spring AOP mógł wygenerować odpowiednie proxy w runtime, konieczne jest pobranie obiektu z fabryki - w naszym przypadku będzie to ClassPathXmlApplicationContext:


public static void main(final String[] args) {
AbstractApplicationContext context = new ClassPathXmlApplicationContext(
"beans.xml");

SomeModelObject modelObject = (SomeModelObject) context
.getBean("someModelObject");

modelObject.setSomeIntProperty(23);
modelObject.setSomeStringProperty("ala");
}
Zwróćmy uwagę, że bean someModelObject w pliku konfiguracyjnym Spring'a zdefiniowany jest z atrybutem scope="prototype" aby wywołanie context.getBean zwracało za każdym razem nową instancję.

Podobnie jak poprzednio, wynikiem działania metody main jest napis na konsoli:

MethodInterceptor: Zmieniamy wartosc pola someIntProperty
MethodInterceptor: Zmieniamy wartosc pola someStringProperty

1 komentarze:

milus pisze...

Witam

Fajny wpis mam jednak 2 uwagi:
1)jestem ciekaw czy jestes swiadomy czemu w twojej konfiguracji jest element aop:aspectj-autoproxy ?
Zgodnie z dokuemtacja element ten ma słuzyc do odpowiedniego "obrabiania" beanow które maja annotacje @Aspect.
U ciebie takich beanów nie ma ...
(Nie)świadomie wykorzystany został fakt, że w specyficznych przypadkach Spring (znam 2 takie przypadki )autoproxuje beany. Osobiscie uwazam to za dosc duze zagrozenie, zreszta jest to 1 z powodów dlaczego nie polecane jest mieszanie sposobów definiowania konfiguracji Spring AOP.
2) Stosowanie Spring AOP do interceptowania zmian na propertiesach JavaBean uwazam troche za sztuke dla sztuki, poniewaz obiekty modelu (dla których wydaje sie byc sensowne wychwytywac zmiany propertiesów) nie są pod "władaniem" kontenera DI (pomijajac uzywanie @Configurable), a 2 sprawa to, że bardzo często settery są metodami final, a tych Spring AOP (w szczególności Cglib) nie łapie...