出现问题
调用链部署过程中某个项目出现:
Caused by: java.lang.IllegalArgumentException: Can not set static XXX field redis.clients.jedis.Jedis.delegate$5975b70 to XXX
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
Google 之后的结果不是很多,但是bytebuddy的一个Issue也出现了这个问题:
IllegalArgumentException: Can not set static XXX field YYY to XXX
当中解答有这么一句话:
The problam is that a class loaded by two different class loaders constitues two different types.
说白了就是同一个类被不同的类加载器加载了多遍,然后再赋值过程中就出现了上面的情况。
那么既然知道了出错的原因,接下来就要知道为什么会导致同一个类被加载多次。
现象梳理
现在的问题:
- Can not set static com.lagou.pard.agent.plugin.intercept.ClassInstanceMethodInterceptor field redis.clients.jedis.Jedis.delegate$5975b70 to com.lagou.pard.agent.plugin.intercept.ClassInstanceMethodInterceptor
报错的原因:
- ClassInstanceMethodInterceptor 类被不同类加载器加载然后进行赋值出错
是否代码问题:
- 不是,尝试多个tomcat版本,7.0.40,7.0.6,7.0.8正常,7.0.56,7.0.78(线上版本)出现错误。
这里有个问题误导了我,我开始以为版本是这样的
7.0.40 OK
7.0.56 ERROR
7.0.6 OK
7.0.78 ERROR
7.0.8 OK
还奇怪为什么一会好一会坏,后来发现正确的应该是这样:
7.0.6 OK
7.0.8 OK
7.0.40 OK
7.0.56 ERROR
7.0.78 ERROR
7.0.8 是在7.0.78 之前的版本 7.0.8 != 7.0.80
调试:
理论上,根据 https://www.cnblogs.com/xing901022/p/4574961.html ,WEB-INF/lib下的jar文件中的class 由webapp 应用类加载器加载。
- 7.0.6(正常tomcat)调试显示,两个ClassInstanceMethodInterceptor 都是由 Launcher$AppClassLoader 加载的-----
这里和理论冲突(修正:了解之后发现tomcat启动之前的阶段类加载就是AppClassLoader)
- 7.0.78(出错)调试显示,field.set(value)中,value的加载器是Launcher$AppClassLoader,但是field的加载器却是 WebappClassLoader

疑问:
- tomcat的不同版本在类加载上有什么区别?
并且是跨版本出现问题。
- 正常的WEB-INF/lib下的jar文件中的class 由谁加载?应用类加载器
Tomcat:当应用需要到某个类时,则会按照下面的顺序进行类加载:
1 使用bootstrap引导类加载器加载
2 使用system系统类加载器加载
3 使用应用类加载器在WEB-INF/classes中加载
4 使用应用类加载器在WEB-INF/lib中加载
5 使用common类加载器在CATALINA_HOME/lib中加载
Both AppClassLoader and SystemClassLoader are same.
application class loader : java.lang.ClassLoader.getSystemClassLoader() returns this loader
- 理论上来说在WebAppClassLoader加载Interceptor的时候会先自己检查自己是否已经加载过(resourceEntries),然后去父级加载器寻找已经加载过的Class,如果还不存在才会由自己加载。
####尝试
- 解构需要上线的项目:
- 删除不需要的java、不需要的配置直到项目最简--保证错误依然出现
- DEBUG WebAppClassLoader loadClass加载ClassInstanceMethodInterceptor的过程
clazz = this.findLoadedClass0(name);//自己查找一遍
...
clazz = this.findLoadedClass(name);//没找到用JVM再找一遍
...依然没找到
clazz = this.system.loadClass(name);//使用system加载
//这里的system是一个AppClassLoader
//加载成功,因为之前在javaagent阶段拦截tomcat类时已经加载过了
* 7.0.78 [7.0.78 loadClass代码图片](/getImage/IMG20180209172558DU7KPLGCGRJYUJXV3VI4FNNMSH91UA.png "7.0.78 loadClass代码")
clazz = this.findLoadedClass0(name);//自己查找一遍
...
clazz = this.findLoadedClass(name);//没找到用JVM再找一遍
...依然没找到
clazz = this.j2seClassLoader.loadClass(name);//使用j2seClassLoader加载
//这里的j2seClassLoader是一个Launcher$ExtClassLoader
//根据结构来说ExtClassLoader是AppClassLoader的父级,父级不能感知子级加载过的类,所以这里加载会失败,然后后面就开始用WebAppClassLoader自己加载了
* 其他
在7.0.78版本的WebAppClassLoader中发现了这个:
/** @deprecated */
@Deprecated
protected ClassLoader system = null;
所以明白为什么版本之间行为不一致(开始以为7.0.8和7.0.78是相隔很近的两个版本,其实差了好几年!!!!)。
####结论
- 现在很清楚了,类加载机制不是一会好一会坏,而是新版本取消了system 这个ClassLoader;
- 出现问题是因为WebAppClassLoader查找两次查找不到后使用j2seClassLoader加载时,j2seClassLoader(ExtClassLoader)不知道子级AppClassLoader已经加载过了。
####解决方案
- 降级Tomcat:否定。tomcat新特性需要保留,不能因为agent降级tomcat,且线上原因不允许大动作。
- 将agent.jar移出WEB-INF/lib 文件夹,这样就不会通过
WebAppClassLoader
加载
- maven 自动化操作:尝试失败,各个项目war包打包有差别,不好统一操作。另外也没找到能将POM的某个jar移出lib目录的插件。
- 手动:不再加入POM而是直接将jar包加入项目源码。更新需要手动更新jar文件。--这是一种不是很好的方。
....TODO