joinpoint - Mezclando proxies JDK y CGLIB dentro de Spring
spring aop joinpoint example (2)
Tengo una aplicación que se ejecuta con Spring, y estoy usando AOP en algunos lugares. Dado que quiero usar la anotación @Transactional en el nivel de la interfaz, tengo que permitir que Spring cree proxies JDK. Por lo tanto, no establezco la propiedad proxy-target-class en true. Por otro lado, no quiero crear una interfaz para todas las clases que deseo: si la interfaz no tiene sentido, quiero tener solo la implementación, y Spring debería crear un proxy CGLIB.
Todo funcionaba perfectamente, tal como lo describí. Pero quería tener algunas otras anotaciones (creadas por mí) que van en las interfaces y que las clases de implementación las "hereden" (al igual que la @Transactional). Resulta que no puedo hacer eso con el soporte incorporado para AOP en Spring (al menos no pude averiguar cómo hacerlo después de algunas investigaciones. La anotación en la interfaz no es visible en la clase de implementación, y De ahí que no se avise a esa clase).
Así que decidí implementar mi propio punto de corte e interceptor , permitiendo que las anotaciones de otros métodos pasen a las interfaces. Básicamente, mi punto de corte busca la anotación en el método y, hasta que no se encuentre, en el mismo método (mismo nombre y tipo de parámetro) de las interfaces que implementa la clase o sus superclases.
El problema es: cuando declaro un bean DefaultAdvisorAutoProxyCreator, que aplicará correctamente este punto de corte / interceptor, se rompe el comportamiento de las clases de asesoría sin interfaces. Aparentemente, algo sale mal y Spring intenta usar mis clases dos veces, una con CGLIB y luego con JDK.
Este es mi archivo de configuración:
<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>
Esta es la clase en la que quiero ser proxy, sin interfaces:
@Component
public class ServiceExecutorImpl
{
@Transactional
public Object execute(...)
{
...
}
}
Cuando intento autowire en algún otro bean, como:
public class BaseService {
@Autowired
private ServiceExecutorImpl serviceExecutorImpl;
...
}
Me sale la siguiente excepción:
java.lang.IllegalArgumentException: Can not set mypackage.ServiceExecutorImpl field mypackage.BaseService.serviceExecutor to $Proxy26
Estas son algunas líneas de la salida Spring:
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]
Podría proporcionar la salida completa si alguien piensa que ayudará. No tengo idea de por qué Spring está tratando de "hacer un doble proxy" de mi clase, y por qué esto simplemente sucede cuando declaro el bean DefaultAdvisorAutoProxyCreator.
He estado luchando con esto por algún tiempo y cualquier ayuda o idea sería muy apreciada.
EDITAR:
Este es mi código fuente de interceptor, según lo solicitado. Básicamente, registra la ejecución del método (solo los métodos anotados con @Trace son interceptados). Si el método se anota con @Trace (falso), el registro se suspende hasta que el método regrese.
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();
}
}
¡Gracias!
Algo no coincide aquí: si hay un $ProxyXX
, significa que hay una interfaz. Asegúrese de que no hay interfaz. Algunas otras notas:
en su corte de puntos puede verificar si el objeto de destino ya es un proxy usando
(x instanceof Advised)
, luego puede convertir aAdvised
puede usar
<aop:scoped-proxy />
para definir la estrategia de proxy por bean
Encontré una solución usando el ''proxy de alcance'' sugerido por Bozho.
Ya que estoy usando casi solo anotaciones, mi clase ServiceExecutor ahora se ve así:
@Component
@Scope( proxyMode = ScopedProxyMode.TARGET_CLASS )
public class ServiceExecutor
{
@Transactional
public Object execute(...)
{
...
}
}
Hasta ahora todo parece estar funcionando bien. No sé por qué tengo que decirle explícitamente a Spring que esta clase debe ser proxy mediante CGLIB, ya que no implementa ninguna interfaz. Tal vez sea un error, no lo sé.
Muchas gracias, Bozho.