Sunday, July 24, 2011

Spring proxy - calling methods within the same service

One of the bigest issues of spring aop is, that when you use its proxies for adding some aop functionality (like for example transactions or security), your calls to a method withing the same bean won't trigger the advised aop functionality.

So if you have a Service, that has two methods, where method A() HAS NO @Transactional annotation and method B() HAS a @Transactional annotation and non transactional method A() calls during its execution transactional method B() then spring won't start any transaction. This is because the spring proxy will redirect its call for A() to the service object but the call to B() won't be executed on the proxy (that knows how and when to start the transaction) but instead on the actual service object that has not functional code to start the transaction (only a @Transactional annotation on the method B()).

To overcome this problem I have implemented a simple solution that injects the proxy of current bean instance and then you execute advised methods calling this instance proxy variable. All you have use is a @ThisInstance annotation and register a custom BeanPostProcessor:
<bean class="sk.yourpackage.ThisInstanceBeanPostProcessor"/>
After this you annotate a setter or field with @ThisInstance annotation and spring will inject a proxy instance of this bean as a setter parameter or field value (if a proxy won't be created it will inject the actual unproxied service). Like for example:
@ThisInstance
private MyService thisInstance;
After this you change your code from:
this.B();
into
thisInstance.B();
And that is the whole configuration. It works for private, protected, ... method and fields and for beans with any scoping.
 
This is the code of the annotation:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface ThisInstance { }
and this is the code for bean post processor:
public class ThisInstanceBeanPostProcessor implements BeanPostProcessor, Ordered {

private final Set<Class<? extends Annotation>> annotationTypes = new LinkedHashSet<Class<? extends Annotation>>();

private int order = Ordered.LOWEST_PRECEDENCE;


public ThisInstanceBeanPostProcessor() {
this.annotationTypes.add(ThisInstance.class);
}


@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}


@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

Object targetBean = getTargetBean(bean);

injectCurrentInstance(targetBean, bean);

return bean;
}


private Object getTargetBean(Object bean) {
Object target = bean;

if (target instanceof Advised) {
target = ((Advised) target).getTargetSource();

if (target instanceof TargetSource) {
try {
target = ((TargetSource) target).getTarget();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}

return target;
}


private void injectCurrentInstance(Object targetBean, Object thisInstance) {

if (annotationTypes.size() == 0) {
return;
}

Class<?> beanClass = targetBean.getClass();

do {
// for each interface - look for injection annotations
for (Class beanInterface : beanClass.getInterfaces()) {
for (Method method : beanInterface.getMethods()) {
for (Class<? extends Annotation> annotationType : annotationTypes) {
if (method.getAnnotation(annotationType) != null) {
invokeMethod(targetBean, method, thisInstance);
break;
}
}
}
}

// for each method - look for injection annotations
for (Method method : beanClass.getDeclaredMethods()) {
for (Class<? extends Annotation> annotationType : annotationTypes) {
if (method.getAnnotation(annotationType) != null) {
invokeMethod(targetBean, method, thisInstance);
break;
}
}
}

// for each field - look for injection annotations
for (Field field : beanClass.getDeclaredFields()) {
for (Class<? extends Annotation> annotationType : annotationTypes) {
if (field.getAnnotation(annotationType) != null) {
setFieldValue(targetBean, field, thisInstance);
break;
}
}
}

beanClass = beanClass.getSuperclass();

} while (!Object.class.equals(beanClass));
}


private void invokeMethod(Object object, Method method, Object... values) {
boolean isAccessible = method.isAccessible();

try {
method.setAccessible(true);

method.invoke(object, values);
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
method.setAccessible(isAccessible);
}
}


private void setFieldValue(Object object, Field field, Object value) {
boolean isAccessible = field.isAccessible();

try {
field.setAccessible(true);

field.set(object, value);
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
field.setAccessible(isAccessible);
}
}


/**
* allows you to add custom annotation type
*
* @param annotationType custom annotation type
*/
public void setAnnotationType(Class<? extends Annotation> annotationType) {
this.annotationTypes.add(annotationType);
}


/**
* allows you to define custom annotation types
*
* @param annotationTypes custom annotation types
*/
public void setAnnotationTypes(Set<Class<? extends Annotation>> annotationTypes) {
this.annotationTypes.clear();
this.annotationTypes.addAll(annotationTypes);
}


@Override
public int getOrder() {
return order;
}


public void setOrder(int order) {
this.order = order;
}
}

5 comments:

  1. Wow, this is really awesome! Thanks so much for your solution - this is a problem that has bugged me for some time now. You rock!

    ReplyDelete
  2. This is really sweet! I took used this code to take advantage of using ehcache for a method that is used in the same service

    ReplyDelete
  3. this works perfect! Thank you!
    I used this for read recursively and with @Async a tree.

    Thank you!!!

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. Very useful piece of code!!
    I have used it for caching in proxy mode still supporting self invocation of @Cacheable services.

    Thanks!

    ReplyDelete