你好,我是看山。
transmittable-thread-local 是阿里开源一个线程池复用场景下,处理异步执行时上下文传递数据问题的解决方案。可以从官方文档https://github.com/alibaba/transmittable-thread-local获取更多信息。
本文主要是变更 transmittable-thread-local 使用方式时出现的一个异常。
异常现场
看异常之前,先简单说下项目大概情况。
项目是 Java 栈,使用了 SpringBoot+MyBatis 的框架结构,构建工具是 Maven。因为项目中使用了比较多的多线程逻辑,所以引入了 transmittable-thread-local,解决上下文传递数据问题。后来做项目升级,接入公司的监控系统,启动时增加了启动参数-javaagent:/path/to/transmittable-thread-local-2.12.1.jar
,通过零侵入的方式解决多线程上下文传值问题。
于是,有些逻辑出错了。
我们看看异常栈(日志做了删改,隐藏项目信息):
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor' available: expected single matching bean but found 3: executor1,executor2,executor3
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1200)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:420)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1127)
……
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
……
复制代码
异常日志很清楚,就是通过AbstractApplicationContext.getBean
获取 Bean 的时候,因为存在多个同类型的ThreadPoolTaskExecutor
,Spring 容器不知道返回哪个 Bean,就抛出了NoUniqueBeanDefinitionException
异常。
排查问题
我们再来看看调用代码:
public static void doSth(Object subtag, Object extra, long time) {
ApplicationContextContainer.getBean(ThreadPoolTaskExecutor.class)
.execute(() -> {
// 一些业务代码
});
}
@Component
public class ApplicationContextContainer implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextContainer.applicationContext = applicationContext;
}
}
复制代码
可以看出来,applicationContext.getBean
时只传入了 class 类型,没有指明 Bean 的名字。推测是项目中定义了多个ThreadPoolTaskExecutor
类型的 Bean,名字分别是 executor1、executor2、executor3(名字改过了,大家写代码时尽量使用见名知意的起名方式)。
@Configuration
public class ExecutorConfig {
@Bean(value = "executor1")
public Executor executor1() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 一些初始化方法
taskExecutor.initialize();
return taskExecutor;
}
@Bean(value = "executor2")
public Executor executor2() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 一些初始化方法
taskExecutor.initialize();
return TtlExecutors.getTtlExecutor(taskExecutor);
}
@Bean(value = "executor3")
public Executor executor3() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 一些初始化方法
taskExecutor.initialize();
return TtlExecutors.getTtlExecutor(taskExecutor);
}
}
复制代码
从上面的代码可以发现,确实有 executor1、executor2、executor3 三个Executor
,executor1 是ThreadPoolTaskExecutor
类型的,executor2 和 executor3 是经过TtlExecutors.getTtlExecutor
包装的ThreadPoolTaskExecutor
。
我们来看看TtlExecutors.getTtlExecutor
方法:
public static Executor getTtlExecutor(@Nullable Executor executor) {
if (TtlAgent.isTtlAgentLoaded() || null == executor || executor instanceof TtlEnhanced) {
return executor;
}
return new ExecutorTtlWrapper(executor, true);
}
复制代码
根据错误反推,经过TtlExecutors.getTtlExecutor
之后返回的还是ThreadPoolTaskExecutor
类型。也就是上面代码走了if
语句,直接返回了输入参数。
但是,这里就碰到了两个开发十大未解之谜中的两个:
代码没改,之前好好地,怎么就报错了;
本地好使,为什么放在服务器上就报错了。
定位问题
首先,我们需要知道,代码的终点不是玄学。我们现在用的计算机还不会撒谎,只要报错了,就一定是有问题。
我们仔细看看TtlExecutors.getTtlExecutor
方法中的if
判断:
TtlAgent.isTtlAgentLoaded():这个是判断 ttlAgentLoaded 标识,这个后文再说;
null == executor:输入参数为 null,显然不符合;
executor instanceof TtlEnhanced:输入参数是TtlEnhanced
类型,输入的是ThreadPoolTaskExecutor
类型,不符合。
所以,重点看看 ttlAgentLoaded 标识:
public static boolean isTtlAgentLoaded() {
return ttlAgentLoaded;
}
复制代码
从全局找到修改ttlAgentLoaded
的地方是:
public final class TtlAgent {
public static void premain(final String agentArgs, @NonNull final Instrumentation inst) {
kvs = splitCommaColonStringToKV(agentArgs);
Logger.setLoggerImplType(getLogImplTypeFromAgentArgs(kvs));
final Logger logger = Logger.getLogger(TtlAgent.class);
try {
logger.info("[TtlAgent.premain] begin, agentArgs: " + agentArgs + ", Instrumentation: " + inst);
final boolean disableInheritableForThreadPool = isDisableInheritableForThreadPool();
// 省略非相关代码
ttlAgentLoaded = true;
} catch (Exception e) {
String msg = "Fail to load TtlAgent , cause: " + e.toString();
logger.log(Level.SEVERE, msg, e);
throw new IllegalStateException(msg, e);
}
}
// 省略非相关代码
}
复制代码
有一定 javaagent 知识的应该知道,premain
方法是 java 启动时,加载 javaagent 后执行的方法。
这就吻合了。
报错之前的服务器代码,ExecutorConfig
类中定义的 executor1 是ThreadPoolTaskExecutor
类型,executor2 和 executor3 是ExecutorTtlWrapper
类型,使用applicationContext.getBean(clazz)
能够得到名字是 executor1 的 Bean。
然后使用-javaagent:/path/to/transmittable-thread-local-2.12.1.jar
方式实现零侵入的transmittable-thread-local
注入能力。ExecutorConfig
类中定义的 executor2 和 executor3 是ThreadPoolTaskExecutor
类型,使用applicationContext.getBean(clazz)
就会查到三个ThreadPoolTaskExecutor
类型的 Bean,Spring 容器没有办法判断返回哪一个,于是抛出了NoUniqueBeanDefinitionException
异常。
本地启动是加上-javaagent:/path/to/transmittable-thread-local-2.12.1.jar
命令,问题复现。
解决问题
解决上面的报错比较简单,就是使用applicationContext.getBean(beanName, clazz)
方法,通过输入指定的 Bean 的名字和类型,获取确定 Bean,代码修改为:
public static void doSth(Object subtag, Object extra, long time) {
ApplicationContextContainer.getBean("executor1", ThreadPoolTaskExecutor.class)
.execute(() -> {
// 一些业务代码
});
}
复制代码
流水线发版回归测试,问题解决。
青山不改,绿水长流,我们下次见。
推荐阅读
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。
评论