小心 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(() -> {
                // 一些业务代码
            });
}

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

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

  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(() -> {
                // 一些业务代码
            });
}

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

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


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

个人主页:https://www.howardliu.cn
个人博文:小心 transmittable-thread-local 的这个坑
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:小心 transmittable-thread-local 的这个坑

👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻

公众号:看山的小屋