在Spring中,使用ProxyFactory实现对Cglib代理对象的再次代理
背景
公司项目有一个在线测试接口的功能,是使用反射机制实现的
在项目设计架构中,Service层还有一个高层BizService层
并且@Transcational注解只会加在BizService层,即Service层接口不会存在@Transcational注解
需求
要求能够直接通过在线测试接口的功能测试Service层的接口
问题
因基于JPA配置的TransactionManager,当测试不存在@Transcational注解的Service层方法时,抛出无事务支持的异常
Caused by: javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'flush' call
问题分析
首先,@Transcational注解 原理是使用Aop基于原对象默认使用CGLIB生成代理对象(如果注解在Impl类上,而不是接口上),并放入Spring容器中
那么不存在 @Transcational注解 的Service层bean,则是一个不会被 @Transcational注解 aop织入通知并进行代理的bean
可想而知,调用这些方法时也不会存在对应的Transaction,需要Transaction支持的方法自然会抛异常
尝试解决
首先,我尝试了使用JDK动态代理为Service层Bean添加Transcation的处理,代码如下:
Object bean = SpringContextUtils.getBeanByName(beanName);
Object proxyBean = Proxy.newProxyInstance(bean.getClass().getClassLoader(), bean.getClass().getInterfaces(),(proxy, method, args)->{
if (Object.class.equals(method.getDeclaringClass())) {
logger.debug("skip Proxy process, it is Object class method : {}", method.getName());
return method.invoke(bean, arguments);
}
logger.debug("Open Transaction for the method : {}", method.getName());
Object invoke = null;
DefaultTransactionDefinition transDef = new DefaultTransactionDefinition();
//same as the default propagation of spring Annontation @Transcational
transDef.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus transactionStatus = TransactionUtils.getTransaction(transDef);
try {
invoke = method.invoke(bean, arguments);
TransactionUtils.commit(transactionStatus);
logger.debug("Committed Transaction for the method : {}", method.getName());
} catch (Exception e) {
TransactionUtils.rollback(transactionStatus);
logger.debug("Rolled back Transaction for the method : {}", method.getName());
throw e;
}
return invoke;
});
一开始的确可以实现添加Transcation的处理的代理功能
但是这时我发现,如果这个Bean中,只要 任意一个方法 有 @Transcational注解 ,那么这个Bean就会被spring处理,生成代理对象放入Spring容器中
并且在这个Bean为代理对象情况下,我的JDK动态代理就起不来作用了。无法为这个代理对象再次进行多一个的代理。而后我尝试使用CGLIB动态代理,也存在一样的情况
排查后,发现由于 Cglib 本身的设计,无法实现在 Proxy 外面再包装一层 Proxy,通常会报如下错误:
Caused by: java.lang.ClassFormatError: Duplicate method name "newInstance" with signature "..........
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
... 10 more
错误来源代码:
net.sf.cglib.proxy.Enhancer#generateClass(ClassVisitor v)
......
// 以下部分的字节码,每次生成 Proxy 实例都会插入。JVM 验证字节码时则会报错。
if (useFactory || currentData != null) {
int[] keys = getCallbackKeys();
emitNewInstanceCallbacks(e);
emitNewInstanceCallback(e);
emitNewInstanceMultiarg(e, constructorInfo);
emitGetCallback(e, keys);
emitSetCallback(e, keys);
emitGetCallbacks(e);
emitSetCallbacks(e);
}
解决方法
在查阅资料后,发现spring自带一个强大的AOP代理功能,即 ProxyFactory
它能够在代理对象上再次进行代理,并且之前的切面代码依旧能够使用
具体代码如下:
Object bean = SpringContextUtils.getBeanByName(beanName);
//get proxy Object
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvice((MethodInterceptor) (invocation) -> {
if (Object.class.equals(invocation.getMethod().getDeclaringClass())) {
logger.info("skip Proxy process, it is Object class method : {}", invocation.getMethod().getName());
return invocation.proceed();
}
logger.info("Open Transaction for the method : {}", invocation.getMethod().getName());
Object invoke = null;
DefaultTransactionDefinition transDef = new DefaultTransactionDefinition();
//same as the default propagation of spring Annontation @Transcational
transDef.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus transactionStatus = TransactionUtils.getTransaction(transDef);
try {
invoke = invocation.proceed();
TransactionUtils.commit(transactionStatus);
logger.info("Committed Transaction for the method : {}", invocation.getMethod().getName());
} catch (Exception e) {
TransactionUtils.rollback(transactionStatus);
logger.info("Rolled back Transaction for the method : {}", invocation.getMethod().getName());
throw e;
}
return invoke;
});
//获取完成代理的代理对象
Object proxyBean = proxyFactory.getProxy();
测试再次代理之前(即完成添加Transcation的代理之前)的切面代码是否可用,自定义一个AOP类:
@Aspect
@Component
public class MyTestAop {
@Pointcut(value = "execution(* com.xxx.common.services.core.xxxService.updateXxx(..))")
public void stepBasePoint() {
}
@Before("stepBasePoint()")
public void before() throws Throwable {
System.out.println("My Test Aop");
}
}
重启项目,测试在线测试接口的功能,结果如下,自定义AOP类的切面代码可以正常调用,并且不再抛出无事务支持的异常