I have an application running with Spring, and I'm using AOP in some places. Since I want to use the @Transactional annotation at interface level, I have to allow Spring to create JDK proxies. So, I don't set the proxy-target-class property to true. On the other hand, I don't want to create an interface for every single class I want advised: if the interface just doesn't make sense, I want to have just the implementation, and Spring should create a CGLIB proxy.
Everything was working perfectly, just as I described. But I wanted to have some other annotations (created by me) going in interfaces and being "inherited" by the implementation classes (just like the @Transactional one). Turns out that I can't do that with the built-in support for AOP in Spring (at least I could not figure it out how to do it after some research. The annotation in the interface is not visible in the implementation class, and hence that class does not get advised).
So I decided to implement my own pointcut and interceptor, allowing other method annotations to go on interfaces. Basically, my pointcut look for the annotation on the method and, until not found, in the same method (same name and parameter types) of the interfaces that the class or its superclasses implements.
The problem is: when I declare a DefaultAdvisorAutoProxyCreator bean, that will properly apply this pointcut/interceptor, the behavior of advising classes with no interfaces is broken. Apparently something goes wrong and Spring tries to proxy my classes twice, once with CGLIB and then with JDK.
This is my configuration file:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd">
<!-- Activates various annotations to be detected in bean classes: Spring's
@Required and @Autowired, as well as JSR 250's @Resource. -->
<context:annotation-config />
<context:component-scan base-package="mypackage" />
<!-- Instruct Spring to perform declarative transaction management automatically
on annotated classes. -->
<tx:annotation-driven transaction-manager="transactionManager" />
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
<bean id="logger.advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<constructor-arg>
<bean class="mypackage.MethodAnnotationPointcut">
<constructor-arg value="mypackage.Trace" />
</bean>
</constructor-arg>
<constructor-arg>
<bean class="mypackage.TraceInterceptor" />
</constructor-arg>
</bean>
</beans>
This is the class I want to be proxied, with no interfaces:
@Component
public class ServiceExecutorImpl
{
@Transactional
public Object execute(...)
{
...
}
}
When I try to autowire it in some other bean, like:
public class BaseService {
@Autowired
private ServiceExecutorImpl serviceExecutorImpl;
...
}
I get the following exception:
java.lang.IllegalArgumentException: Can not set mypackage.ServiceExecutorImpl field mypackage.BaseService.serviceExecutor to $Proxy26
This are some lines of the Spring output:
13:51:12,672 [main] DEBUG [org.springframework.aop.framework.Cglib2AopProxy] - Creating CGLIB2 proxy: target source is SingletonTargetSource for target object [mypackage.ServiceExecutorImpl@1eb515]
...
13:51:12,782 [main] DEBUG [org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'serviceExecutorImpl' with 0 common interceptors and 1 specific interceptors
13:51:12,783 [main] DEBUG [org.springframework.aop.framework.JdkDynamicAopProxy] - Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [mypackage.ServiceExecutorImpl$$EnhancerByCGLIB$$2eb5f51@5f31b0]
I could supply the full output if someone thinks it will help. I have no idea why Spring is trying to "double-proxy" my class, and why this just happens when I declare the DefaultAdvisorAutoProxyCreator bean.
I have been struggling with this for some time now, and any help or ideas would be very much appreciated.
EDIT:
This is my interceptor source code, as requested. It basically log the method execution (only methods annotated with @Trace get intercepted). If the method is annotated with @Trace(false), the logging is suspended until the method returns.
public class TraceInterceptor
implements
MethodInterceptor
{
@Override
public Object invoke(
MethodInvocation invocation )
throws Throwable
{
if( ThreadExecutionContext.getCurrentContext().isLogSuspended() ) {
return invocation.proceed();
}
Method method = AopUtils.getMostSpecificMethod( invocation.getMethod(),
invocation.getThis().getClass() );
Trace traceAnnotation = method.getAnnotation( Trace.class );
if( traceAnnotation != null && traceAnnotation.value() == false ) {
ThreadExecutionContext.getCurrentContext().suspendLogging();
Object result = invocation.proceed();
ThreadExecutionContext.getCurrentContext().resumeLogging();
return result;
}
ThreadExecutionContext.startNestedLevel();
SimpleDateFormat dateFormat = new SimpleDateFormat( "dd/MM/yyyy - HH:mm:ss.SSS" );
Logger.log( "Timestamp: " + dateFormat.format( new Date() ) );
String toString = invocation.getThis().toString();
Logger.log( "Class: " + toString.substring( 0, toString.lastIndexOf( '@' ) ) );
Logger.log( "Method: " + getMethodName( method ) );
Logger.log( "Parameters: " );
for( Object arg : invocation.getArguments() ) {
Logger.log( arg );
}
long before = System.currentTimeMillis();
try {
Object result = invocation.proceed();
Logger.log( "Return: " );
Logger.log( result );
return result;
} finally {
long after = System.currentTimeMillis();
Logger.log( "Total execution time (ms): " + ( after - before ) );
ThreadExecutionContext.endNestedLevel();
}
}
// Just formats a method name, with parameter and return types
private String getMethodName(
Method method )
{
StringBuffer methodName = new StringBuffer( method.getReturnType().getSimpleName() + " "
+ method.getName() + "(" );
Class<?>[] parameterTypes = method.getParameterTypes();
if( parameterTypes.length == 0 ) {
methodName.append( ")" );
} else {
int index;
for( index = 0; index < ( parameterTypes.length - 1 ); index++ ) {
methodName.append( parameterTypes[ index ].getSimpleName() + ", " );
}
methodName.append( parameterTypes[ index ].getSimpleName() + ")" );
}
return methodName.toString();
}
}
Thanks!
I found a solution using the 'scoped-proxy' suggested by Bozho.
Since I'm using almost only annotations, my ServiceExecutor class now looks like this:
@Component
@Scope( proxyMode = ScopedProxyMode.TARGET_CLASS )
public class ServiceExecutor
{
@Transactional
public Object execute(...)
{
...
}
}
Until now everything seens to be working fine. I don't know why I have to explicitly tell Spring this class should be proxied using CGLIB, since it does not implement any interface. Maybe it's a bug, I don't know.
Thanks a lot, Bozho.