一, 前言
这篇是类加载器相关的第三篇:
实战分析 Tomcat 的类加载器结构 (使用 Eclipse MAT 验证) 还是 Tomcat, 关于类加载器的趣味实验
昨天下午刚写了篇 类加载器相关的, 晚上想着验证个问题: Tomcat 跑了多个 spring web 项目, 那么 org.springframework.Web.servlet.DispatcherServlet 这种类是怎么个情况呢? 多个不同类加载器加载的, 同时存在的同名类?
我是打算利用阿里开源的 arthas 工具来查看的, 但是这个工具只支持 Linux. 说来也不怕让人笑话, 公司的后端服务, 开发环境, 测试环境用的 Windows 的, 以后交付给客户不知道是用啥. 先不说这个吧, 反正我们打的 war 包, 在 Windows 服务器的 tomcat 上没什么问题.
但是当我把同样的 war 包丢到 Linux 上时, 发现报错了, 没启动成功....hahhah... 尴尬...
错误如下:
- Caused by: java.lang.NoSuchMethodError: javax.persistence.Table.indexes()[Ljavax/persistence/Index;
- at org.hibernate.cfg.annotations.EntityBinder.processComplementaryTableDefinitions(EntityBinder.java:936)
- at org.hibernate.cfg.AnnotationBinder.bindClass(AnnotationBinder.java:824)
- at org.hibernate.cfg.Configuration$MetadataSourceQueue.processAnnotatedClassesQueue(Configuration.java:3790)
- at org.hibernate.cfg.Configuration$MetadataSourceQueue.processMetadata(Configuration.java:3744)
- at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1410)
- at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1844)
- at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1928)
- at org.springframework.ORM.hibernate4.LocalSessionFactoryBuilder.buildSessionFactory(LocalSessionFactoryBuilder.java:372)
- at org.springframework.ORM.hibernate4.LocalSessionFactoryBean.buildSessionFactory(LocalSessionFactoryBean.java:454)
- at org.springframework.ORM.hibernate4.LocalSessionFactoryBean.afterPropertiesSet(LocalSessionFactoryBean.java:439)
- at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1687)
- at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1624)
- ... 38 common frames omitted
大概意思是, javax.persistence.Table 的 indexes()方法不存在.
二, 排查过程
首先, 我在 idea 中搜了一把 "javax.persistence.Table", 搜到的结果是, hibernate-jpa-2.1-API-1.0.0.Final.jar 这里面有个同名的类, 看了下, indexes()方法是存在的.
好吧, 学了一阵子类加载器了, 我觉得, 首先还是看看, 这个类是从哪加载的吧. 懒得去加 -XX:+TraceClassLoading 参数了, 直接 用阿里的神器, greys(用 arthas 也可以, arthas 是基于 greys 搞的) 挂载上去, 用下面的命令搜索了一下.
- ga?>sc -df javax.persistence.Table
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | class-info | javax.persistence.Table |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | code-source | /home/upload/apache-tomcat-8.5.28/webapps/CAD-WebService/Web-INF/lib/persistence-API |
- | | -1.0.jar |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | name | javax.persistence.Table |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | isInterface | true |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | isAnnotation | true |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | isEnum | false |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | isAnonymousClass | false |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | isArray | false |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | isLocalClass | false |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | isMemberClass | false |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | isPrimitive | false |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | isSynthetic | false |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | simple-name | Table |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | modifier | abstract,interface,public |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | annotation | java.lang.annotation.Target,java.lang.annotation.Retention |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | interfaces | java.lang.annotation.Annotation |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | super-class | |
- +----------------------------------------------------+----------------------------------------------------------------------------------+
- | class-loader | ParallelWebappClassLoader |
从上图看出来, javax.persistence.Table 这个类啊, 是 webappclassloader 从 webapps/CAD-WebService/Web-INF/lib/persistence-API -1.0.jar 加载的.
于是我打开 这个 jar 包看了下, 里面确实有 javax.persistence.Table , 这个类也确实没有 indexes()方法:
看来问题就在这里, 是加载到了错误的 jar 包. 接下来的处理, 就要结合业务代码, 看看到底是从哪引入了这个包, 这个包是否需要, 不需要的话, 直接排除掉即可.(可使用 idea 插件 maven helper).
如果只是 尽快解决问题, 一般到这步就可以了. 但我奇怪的是, Windows 上为啥没问题呢???(黑人问号)
后边在 Windows 的 tomcat 启动脚本加了 -XX:+TraceClassLoading, 发现, 该类是从 hibernate 那个 jar 包加载的, 所以没问题.(要让 Windows 上输出类加载日志, 要修改点东西. https://www.cnblogs.com/welcomer/p/5068340.html)
三, 根因分析
我看了下代码, 这个 jar 包, 确实需要, 不能排除掉... 只是比较奇怪, 在 Linux 上, 为啥会优先加载了 persitance-API.jar, 难道在 Windows 没有先加载 persistence-API.jar?
带着这些疑问, 我恶向胆边生, 直接 dump 了 Windows 下和 Linux 的堆内存.
jmap -dump:live,format=b,file=heap3.bin 123072 -----Linux 的
jmap -dump:live,format=b,file=heap-Windows.bin 11640 --Windows 的
eclipse mat 一把打开 Linux 的堆 dump 后, 用 oql 语句, 查询了一下所有的 ParallelWebappClassLoader:
好, 再看看 Windows 的, 操作和上面差不多, 直接看结果:
上图可见, Windows 上, 是按字母序来的, hibernate 那个包, 妥妥地排在 persistence-API.jar 的前面... 这让人不得不吐槽下, 这个顺序怎么搞的, Linux 上文件感觉跟乱序一样...
由于 tomcat 8 才有 localRepositories 这个字段, 我这里没有可运行的源码, 所以只能大概看看 spring-boot 内嵌的 tomcat jar 包的源码了, 大概是这么个方法:
- org.apache.catalina.loader.WebappClassLoaderBase#start
- public void start() throws LifecycleException {
- state = LifecycleState.STARTING_PREP;
- WebResource classes = resources.getResource("/WEB-INF/classes");
- if (classes.isDirectory() && classes.canRead()) {
- localRepositories.add(classes.getURL());
- }
- WebResource[] jars = resources.listResources("/WEB-INF/lib");
- for (WebResource jar : jars) {
- if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
- localRepositories.add(jar.getURL());
- jarModificationTimes.put(
- jar.getName(), Long.valueOf(jar.getLastModified()));
- }
- }
- state = LifecycleState.STARTED;
- }
上面标红处, 就是 去 /Web-INF/lib 下面获取所有的 jar 包, 然后遍历, 加入到 localRepositories. 这里看来, 去读文件系统后, 没有根据文件名排序吧... 而正好呢, Windows 下和 Linux 下返回的文件列表, 顺序不同.
四, 总结
综上, 可以大概总结下, 一般来说, 不同操作系统返回的文件, 顺序都是不太一致的, 如果代码里, 直接依赖了这种顺序, 就会出现这类:
测试: 小哥哥, 你程序有 bug...
你: 不可能, 我这好好的...
测试: 小哥哥, 不骗你, 你过来看嘛...
你: 不看不看, 烦不烦??
想到以前遇到的一个 spring 循环依赖的问题(Linux 上不行, Windows 上可以), 应该也是这个原因... 哎.. 恼火
来源: https://www.cnblogs.com/grey-wolf/p/11028975.html