❝这是一个普通的一天。
我丝滑且迅速的写下这段代码,自信满满的告诉QA,说没问题了,可以测了。(养成自测的良好习惯)

结果...




一看到这个错,我心想不应该啊,肯定是不会忘记开启exposeProxy
这种小失误啊。

不过看到后半段错误提示:"ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context."
立马反应到可能是因为调用事务方法的线程是mq消费线程池里的锅
想起之前领导对我的谆谆教诲,做学问要严谨,一通源码加调试分析,果不其然,就是这个原因,也让我长了教训!!!
注解方式执行事务本质就是 对该类开启代理对象(spring aop 或者 Aspectj-jdk动态代理或者cglib),然后通过调用代理对象,来执行事务方法。
所以如果我们在本类中,调用了事务注解的方法,是不会生效的。
一般有两个解决方案:
把事务方法单独写到另外的单独类中 使用 spring提供的 AopContext
,currentProxy()
,它可以帮助我们获取到对应的代理类。
/**
* ThreadLocal holder for AOP proxy associated with this thread.
* Will contain {@code null} unless the "exposeProxy" property on
* the controlling proxy configuration has been set to "true".
* @see ProxyConfig#setExposeProxy
*/
private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");
AopContent
通过ThreadLocal 保存了每个代理对象,所以如果没有开启这个功能或者 当前线程中获取不到 这个代理对象就会抛出 一开始的错误异常。开启这个功能也很简单,在spring boot启动类上加上注解
@EnableAspectJAutoProxy(exposeProxy = true)
即可。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
/**
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
* to standard Java interface-based proxies. The default is {@code false}.
*/
boolean proxyTargetClass() default false;
/**
* Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal}
* for retrieval via the {@link org.springframework.aop.framework.AopContext} class.
* Off by default, i.e. no guarantees that {@code AopContext} access will work.
* @since 4.3.1
*/
boolean exposeProxy() default false;
}
如注解描述,proxyTargetClass
来决定是否强制使用cglib来实现aop,而exposeProxy
则是控制是否暴露代理类,如果开启的话,就能使用AopContext
工具在本类中获取到代理对象。
ps: 注解上这两个配置,决定了spring aop自动创建代理器的处理方式,具体可以参考AbstractAdvisorAutoProxy
这个类,
或者这篇博主的文章 https://www.cnblogs.com/foreveravalon/p/8653832.html
但是!
spring为了实现能在本类,准确来说,是为了能在本线程中去获取代理对象。需要在threadLocal
中去维护当前代理类。
那代理类是什么时机去set进去的呢?
通过追踪,currentProxy
的set()
方法,在CglibAopProxy
和JdkDynamicAopProxy
中都发现了它的使用。并且两个的处理方式是基本一致的。
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;
Object target = null;
TargetSource targetSource = this.advised.getTargetSource();
try {
if (this.advised.exposeProxy) {
// 如果exposeProxy配置是开启的,在当前线程中保存当前代理名称
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
// 切面执行逻辑代码
...
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
...
retVal = methodProxy.invoke(target, argsToUse);
}
else {
...
retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
}
retVal = processReturnType(proxy, target, method, retVal);
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// threadLoacl 里替换成上一个 代理对象的 name
AopContext.setCurrentProxy(oldProxy);
}
}
}
@Nullable
static Object setCurrentProxy(@Nullable Object proxy) {
Object old = currentProxy.get();
if (proxy != null) {
currentProxy.set(proxy);
}
else {
currentProxy.remove();
}
return old;
}
总结来说,如果开启了expose proxy
,spring会做如下处理:
先在 ThreadLocal
中保存当前代理对象A然后执行业务逻辑(业务方法可能会调用另一个代理对象B) 更新当前线程代理对象(更新当前线程代理对象为B)
所以,这个案例的错误原因就是:
因为我们开启了暴露代理配置,当调用接口,使用@PostMapping()Controller
, spring aop会设置当前线程A(tomact 或者undertow 容器的线程)的代理对象为$Controller
,而rmq线程B在执行注解事务方法A时,通过AopContext
去获取线程A中的代理对象,自然是获取不到。
了解原理后,就知道如何对症下药了,两种方式:
把事务方法抽出去写在一个单独的类中 使用手动事务。
因为我这个 consumer类业务逻辑已经很单一了,所以写到外面的类,不合适。
所以,我采用了手动事务的方式。
/**
* 处理退款完成 消息
* created by mayibz on 2021/1/19
*/
@Service
@Slf4j
public class OSCRefundApplyConsumer extends OrderStateChangeConsumer {
...
@Override
protected String getActionTag() {
return actionTag;
}
@Override
protected String getConsumerGroupName() {
return groupName;
}
@Override
protected void processWithAction(OrderStateChangeContent changeContent) {
TransactionStatus transaction = dataSourceTransactionManager.getTransaction(new DefaultTransactionDefinition());
try {
...
// 业务块
}
dataSourceTransactionManager.commit(transaction);
}catch (Exception e) {
dataSourceTransactionManager.rollback(transaction);
throw e;
}
}
在重写的时候,也顺便调整了rmq使用不同的处理事件作为tag去投递消息,这样我的消费端,逻辑职责更为单一。

总结:
spring 提供的注解事务,虽然方便,但是如果不清楚原理和逻辑,很有可能会埋个大雷。
- 不要直接在本类中"直接"调用注解事务方法
- 不要在多线程下, 使用AopContext.getCurrentProxy()




