当前位置:网站首页>小心 transmittable-thread-local 的这个坑
小心 transmittable-thread-local 的这个坑
2022-07-30 11:25:00 【InfoQ】
你好,我是看山。
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(() -> {
// 一些业务代码
});
}
流水线发版回归测试,问题解决。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注公众号「看山的小屋」。
边栏推荐
- 【ASP.NET Core】选项类的依赖注入
- 物联网技术概论:第6章
- Microsoft SQL服务器被黑客入侵 带宽被窃取
- 基于声信道分析的电缆隧道人员定位技术
- contentDocument contentWindow,canvas 、svg,iframe
- Voltage relay h2d SRMUVS - 100 vac - 2
- UE5 GAS 学习笔记 后记0
- Verilog语法基础HDL Bits训练 07
- Horizontal comparison of 5 commonly used registration centers, whether it is used for interviews or technical selection, is very helpful
- 基于空间特征选择的水下目标检测方法
猜你喜欢
随机推荐
LeetCode_236_二叉树的最近公共祖先
Verilog grammar basics HDL Bits training 08
基于时延估计的扰动卡尔曼滤波器外力估计
Manage reading notes upward
Underwater target detection method based on spatial feature selection
数字量输入输出模块DAM-5088
Meituan internal push + school recruitment written test + summary of knowledge points
反转链表-迭代反转法
idea的package没有空心
《跟唐老师学习云网络》 - 问题定位 - 主机通但容器不通
ODrive应用 #4 配置参数&指令「建议收藏」
TensorFlow自定义训练函数
Interviewer: Redis bloom filter and the cuckoo in the filter, how much do you know?
向上管理读书笔记
STM32F1读取MLX90632非接触式红外温度传感器
高能产出!腾讯内部的MyCat中间件手册,理论实操齐下
salesforce使用方法(salesforce authenticator下载)
SCM engineers written questions induction summary
程序环境和预处理(详解)
Verilog grammar basics HDL Bits training 07



![[ASP.NET Core] Dependency Injection for Option Classes](/img/3f/820b6e33897cf385c3206c02d741f8.png)




