一、背景
日常真线错误日志排查中,日志经常出现,warn级别的错误日志,如下 frozen class cannot edit,但是不影响业务,太多的warn日志,影响真线的其他问题排查,因此需要进一步分析排查原因;
图1
问题:
为什么dubbo的consumer端没有配置validation=true 默认是false,会有校验器进行参数校验?
二、源码分析
出现该问题是在应用某列表页或详情页调用了下面接口:
图2
图3
根据异常堆栈信息,进行分析;(提前说下:导致该问题出现的原因与类加载器有关)
step1、首次加载获取validator,属性中的clazz加载器是LaunchedURLClassLoader, 不是当前线程的TomcatEmbeddedWebappClassLoader
com.alibaba.dubbo.validation.support.AbstractValidation#getValidator
这里clazz对应的是xxx....DepositProjectExternalFacade,见下图
图4
step2、
com.alibaba.dubbo.validation.support.jvalidation.JValidator#JValidator
其中clazz属性通过反射获取
图5
反射使用的是当前线程上下文中的加载器TomcatEmbeddedWebappClassLoader
图6
图7
通过反射获取
TomcatEmbeddedWebappClassLoader的父加载器是LaunchedURLClassLoader,根据双亲委派原则,clazz实际通过父加载器LaunchedURLClassLoader加载,如下图
图8
clazz对象
注:spring 内嵌了tomcat,spring指定TomcatEmbeddedWebappClassLoader的父加载器为LaunchedURLClassLoader
参见:org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory #prepareContext
图9
图10
step3、上面的JValidator校验器初始化加载部分,下面进入正文分析
com.alibaba.dubbo.validation.support.jvalidation.JValidator#validate(java.lang.String, java.lang.Class<?>[], java.lang.Object[])
图11
step4、接着进入
com.alibaba.dubbo.validation.support.jvalidation.JValidator#getMethodParameterBean
图12
首先假设第一次调用xxx.....DepositProjectExternalFacade(应用重启以后第一次调用)
上图中的1 parameterClass = (Class<?>) Class.forName(parameterClassName, true, clazz.getClassLoader());
parameterClassName = xxx......DepositProjectExternalFacade$QueryPaymentParameter
图13
如上图 clazz.getClassLoader()的加载器为LaunchedURLClassLoader(step1中已经分析过),
首次加载,当前jvm中没有parameterClassName这个class,因此通过反射获取该class对象会报错,提示classNotFoundException
图14
然后dubbo做了个try catch,如上图的第2步,CtClass ctClass = pool.makeClass(parameterClassName);
对异常捕获后利用字节码javassist工具,动态生成字节码,相关知识点网上比较多,可以自行查找,限于篇幅本文不做扩展
可以参考参考文献1、参考文献2(这里会涉及到冻结frozen 和 解冻defrost的概念 本文的核心)上图的第三步 parameterClass = ctClass.toClass();当CtClass 调用writeFile()、toClass()、toBytecode() 这些方法的时候,Javassist会冻结CtClass Object,对CtClass object的修改将不允许。这个主要是为了警告开发者该类已经被加载,而JVM是不允许重新加载该类的。继续看下ctClass.toClass()
图15
一般是rest请求,当前线程的上下文加载器是TomcatEmbeddedWebappClassLoader,因此第一次加载parameterClass对象的类加载器是TomcatEmbeddedWebappClassLoader;
然后。。。。。;
图16
获取到parameterClass对象,然后返回,所有功能正常;
step5、继续第二次调用
此时如进入到上图,然后进入图中标注1 parameterClass = (Class<?>) Class.forName(parameterClassName, true, clazz.getClassLoader());
上文解释过加载 parameterClass (DepositProjectExternalFacade$QueryPaymentParameter)
这个对象的加载器是TomcatEmbeddedWebappClassLoader,但是clazz.getClassLoader()这个是LaunchedURLClassLoader(父加载器),因此也会报错,对象不是同一个加载器,也不符合双亲委派原则;
此时还会继续走catch的逻辑,pool.makeClass(parameterClassName),此时就要涉及上文提到过的冻结了,ctclass对象在第一次调用的时候已经new 出来了,
同时最后做了toclass,故此时ctclass已经被冻结;再第二次makeClass时会进行是否被冻结的校验,如果已经冻结,会直接报错
图17
图18
真线报错的信息,就是这里抛出的,至此定位到问题所在;
step6、抛出错误后对应用会有什么影响吗?
图19
由图20 可知这里在最外层做了try cache,直接返回null,
图20
如果返回null,图21中圈起来的逻辑就不走了,这个对应的是图5中的@Notnull注解不起作用,因此不影响业务功能;
三、问题分析
3.1 我们来看下开头提出的问题
consumer没有注册validation=true,为什么会在consumer端进行校验我们来看web-reverse中,dubbo初始化加载的配置
com.alibaba.dubbo.registry.integration.RegistryDirectory#toInvokers
图21
com.alibaba.dubbo.registry.integration.RegistryDirectory#mergeUrl 如果consumer没有配置会使用provide端的配置
图22
如果consumer端配置了,会对dubbo-validation配置进行覆盖,如果没有配置就继承provider的值
图23
因此我们可以看到,虽然反向竞价consumer中没有配置validation=true,但是provide配置了这个validation=true,因此导致consumer进行相关的校验
图24
3.2 解决方法
在dubbo的consumer配置中,加上validation=false,关闭消费端校验
注:最新版dubbo已经解决该问题,解决方案,和通过反射获取的类加载保持一致,即当前类的加载器
parameterClass = ctClass.toClass(clazz.getClassLoader(), (ProtectionDomain)null);