写点什么

小心 transmittable-thread-local 的这个坑

作者:看山
  • 2022 年 6 月 30 日
  • 本文字数:3981 字

    阅读完需:约 13 分钟

小心transmittable-thread-local的这个坑

你好,我是看山。


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(() -> {                // 一些业务代码            });}
@Componentpublic 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(名字改过了,大家写代码时尽量使用见名知意的起名方式)。


@Configurationpublic 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语句,直接返回了输入参数。


但是,这里就碰到了两个开发十大未解之谜中的两个:


  1. 代码没改,之前好好地,怎么就报错了;

  2. 本地好使,为什么放在服务器上就报错了。

定位问题

首先,我们需要知道,代码的终点不是玄学。我们现在用的计算机还不会撒谎,只要报错了,就一定是有问题。


我们仔细看看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(() -> {                // 一些业务代码            });}
复制代码


流水线发版回归测试,问题解决。


青山不改,绿水长流,我们下次见。

推荐阅读


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。

发布于: 刚刚阅读数: 3
用户头像

看山

关注

🏆 InfoQ写作平台-签约作者 🏆 2017.10.26 加入

InfoQ签约作者,CSDN 博客专家,公号「看山的小屋」,专注后端开发、架构相关知识分享,个人网站 https://howardliu.cn/。

评论

发布
暂无评论
小心transmittable-thread-local的这个坑_Java’_看山_InfoQ写作社区