一、前言
這篇是類載入器相關的第三篇:
實戰分析Tomcat的類載入器結構(使用Eclipse MAT驗證)
昨天下午剛寫了篇 類載入器相關的,晚上想著驗證個問題: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上可以),應該也是這個原因。。。哎。。惱火