问题环境dubbo:2。7。18java:java8复制代码问题背景 有业务(Java)的同学反馈,在接入了devops的某些javaagent以后会极大概率出现dubbo调用失败,dubbo接口中用到的业务类都提示找不到,导致反序列化失败,部分日志输出如下:〔2023020919:22:58。982〕〔〕〔〕〔DubboServerHandler172。30。86。136:20880thread2〕〔WARN〕〔com。alibaba。com。caucho。hessian。io。SerializerFactory:686〕HessianBurlap:com。seewo。kishframework。page。PageRequestisanunknownclassinorg。springframework。boot。loader。LaunchedURLClassLoader66373f77:java。lang。ClassNotFoundException:com。seewo。kishframework。page。PageRequest复制代码 但是通过内存查看,com。seewo。kishframework。page。PageRequest等失败的类都已经被org。springframework。boot。loader。LaunchedURLClassLoader66373f77加载。 离了个大谱。源码初步分析 异常日志是在com。alibaba。com。caucho。hessian。io。SerializerFactorygetDeserializer(java。lang。String)这个函数中 try代码中首先通过loadSerializedClass加载待反序列化的类,然后用这个classloader获取这个类对应的反序列化对象deserializer。 loadSerializedClass函数首先调用getClassFactory获取单例的classFactory,然后使用这个单例就加载类。 这里的ClassFactory创建的时候传入的classloader是当前线程的ContextClassLoader。publicclassHessian2SerializerFactoryextendsSerializerFactory{publicHessian2SerializerFactory(){}OverridepublicClassLoadergetClassLoader(){returnThread。currentThread()。getContextClassLoader();}}复制代码 在首次初始化以后,反序列的类都是由此SerializerFactory加载。通过内存分析,可以看到,ClassFactory的classloader居然是sun。misc。LauncherAppClassLoader,这个classloader的classpath是不包含springboot管理的BOOTINF下的包的,导致这个AppClassLoader自然加载不到对应的类。 因此经过分析,之前日志里输出是误导的。HessianBurlap:com。seewo。kishframework。page。PageRequestisanunknownclassinorg。springframework。boot。loader。LaunchedURLClassLoader66373f77:java。lang。ClassNotFoundException:com。seewo。kishframework。page。PageRequest复制代码 这里显示是用org。springframework。boot。loader。LaunchedURLClassLoader去加载类找不到,是完全迷惑的,压根就不是用这个类加载器去加载的,而是用ClassFactory中的loader。 现在就要搞清楚,为什么ClassFactory中的loader会不对。debug阶段 通过修改hessianlite的源码,重新替换对应的类,加入了堆栈 看下是谁第一次调用导致了懒加载单例的初始化,第一次调用的堆栈如下: 可以看到DubboServerHandler172。30。86。136thread2这个线程的ContextClassLoader确实不知为何被设置为了sun。misc。LauncherAppClassLoader,然后看更多日志,发现除了这个线程其它的DubboServerHandler线程的ContextClassLoader都是正常的。 这下问题思路基本上清晰了。因为DubboServerHandler172。30。86。136thread2这个线程的ContextClassLoader是sun。misc。LauncherAppClassLoader,这线程最早调用com。alibaba。com。caucho。hessian。io。SerializerFactorygetClassFactory导致SerializerFactory的loader被赋值为了sun。misc。LauncherAppClassLoader,后面的线程也没有机会对单例的SerializerFactory重新赋值。 接下来就是分析,最早的线程DubboServerHandler172。30。86。136thread2是由谁创建的。又因为业务反馈他们去掉devops的dubbo健康检查javaagent以后,就没有再出现,于是把焦点放在了这个插件上。 这个插件的功能也很简单,就是监听一个端口,收到健康检查的请求以后,提交一个Echo任务到dubbo的任务池里,如果任务池没有被占满,可以被执行,那么表示dubbo健康,否则dubbo线程池已经被占满,表示不健康。 EchoTask就是一个Runnable,里面啥都没做 上面的ThreadPoolExecutor是通过字节码注入的方式从dubbo框架中获取的 问题就出在这个健康检查javaagent往dubbo的线程池提交的这个任务。为了弄清楚这个问题,需要一点点JavaContextClassLoader的知识。 线程上下文类加载器由继承自父线程。如果父线程没有设置上下文类加载器,则线程将继承类加载器的默认实现。线程Thread创建的代码如下:privateThread(ThreadGroupg,Runnabletarget,Stringname,longstackSize,AccessControlContextacc,booleaninheritThreadLocals){this。ThreadparentcurrentThread();this。priorityparent。getPriority();if(securitynullisCCLOverridden(parent。getClass()))this。contextClassLoaderparent。getContextClassLoader();elsethis。contextClassLoaderparent。contextClassL}复制代码 往线程池里执行一个runnable的过程,就是调用addWorker创建线程并执行publicclassThreadPoolExecutorextendsAbstractExecutorService{publicvoidexecute(Runnablecommand){intcctl。get();if(workerCountOf(c)corePoolSize){if(addWorker(command,true))创建并执行线程cctl。get();}}privatebooleanaddWorker(RunnablefirstTask,booleancore){WwnewWorker(firstTask);创建workerfinalThreadtw。t。start();启动线程}}privatefinalclassWorkerextendsAbstractQueuedSynchronizerimplementsRunnable{Worker(RunnablefirstTask){setState(1);inhibitinterruptsuntilrunWorkerthis。firstTaskfirstTthis。threadgetThreadFactory()。newThread(this);创建线程}}复制代码 dubbo的ThreadFactory是com。alibaba。dubbo。common。utils。NamedThreadFactory,里面构造了线程名。publicThreadnewThread(Runnablerunnable){StringnamemPrefixmThreadNum。getAndIncrement();ThreadretnewThread(mGroup,runnable,name,0);ret。setDaemon(mDaemo);}复制代码 从上面的代码可以分析出,创建的DubboServerHandler线程上下文类加载器,继承调用newThread的父线程上下文类加载器。 可以用最简单的一个demo来看验证上面的结论。importjava。util。concurrent。ExecutorSimportjava。util。concurrent。EclassMyRunnableimplementsRunnable{publicvoidrun(){System。out。println(TCCL:Thread。currentThread()。getContextClassLoader());}}classMyClassLoaderextendsClassLoader{}publicclassTest01{publicstaticvoidmain(String〔〕args){Thread。currentThread()。setContextClassLoader(newMyClassLoader());ExecutorServiceexecutorsExecutors。newFixedThreadPool(100);executors。execute(newMyRunnable());}}复制代码 上面的代码输出输出TCCL:MyClassLoader48140564复制代码 通过注入org。apache。dubbo。common。threadlocal。NamedInternalThreadFactory。newThread可以看到这个方法创建了DubboServerHandler172。30。102。71:20880thread1线程。 于是这里的情况就很清楚了,因为dubbohealth这个javaagent是由sun。misc。LauncherAppClassLoader加载的,其执行的线程上下文类加载器也是sun。misc。LauncherAppClassLoader,导致项目启动以后,k8s不停发起健康检查触发了javaagent向dubbo线程池提交任务,导致了头几个DubboServerHandler线程(DubboServerHandlerxxthread1、DubboServerHandlerxxthread2等)的上下文成为了sun。misc。LauncherAppClassLoader。 当真实流量进来,被分派到头几个DubboServerHandler处理,此时它第一个调用SerializerFactory的getClassFactory导致ClassFactory的loader被设置为了sun。misc。LauncherAppClassLoader,这个单例开始继续祸害了。 如果初始状态往dubbo线程池提交任务是一个很危险的事情,一定要保障线程的上下文是正确的,不然就悲剧了。修改方法 这里修改方法也简单,只需要将提交任务的时候把自己javaagent的classloader切换为业务的classloader即可。后记 发现旧版本的dubbo没有这个问题,是因为它是用SerializerFactory的loader,而不是ClassFactory中的loader 至于为什么,我就不想知道了。 原文链接:https:juejin。cnpost7199870903534190651