tomcat插件類加載一個(gè)“坑”問題排查

昨天遇到一個(gè)詭異但是很有趣的類加載問題,雖然很快解決了,但是我還是打算剖根問底,分析內(nèi)部問題出現(xiàn)的原因,畢竟類加載機(jī)制雖然說都知道怎么回事,但是還沒在實(shí)戰(zhàn)中實(shí)踐過,也考慮到有個(gè)項(xiàng)目可能需要用到自定義類加載器,趁此機(jī)會(huì)先初步了解一下。

問題描述

  1. 我采用了Servlet3.0,新增加了SPI加載機(jī)制,會(huì)自動(dòng)掃描classpath:META-INF/services/javax.servlet.ServletContainerInitializer中的所有這個(gè)文件,并加載其中的所有javax.servlet.ServletContainerInitializer的實(shí)現(xiàn)類,實(shí)現(xiàn)替換web.xml的功能,讓你的項(xiàng)目war可以不需要web.xml也能正常在tomcat運(yùn)行。
  2. 然后呢,日志我采用了logback,很可愛的是這個(gè)jar中ch.qos.logback.classic.servlet.LogbackServletContainerInitializer就實(shí)現(xiàn)了javax.servlet.ServletContainerInitializer,因此呢,tomcat在啟動(dòng)時(shí)就會(huì)自動(dòng)加載這個(gè)類初始化一些配置。
  3. LogbackServletContainerInitializer是在logback-classic包中的,javax.servlet.ServletContainerInitializer是在javax.servlet-api包中的。
  • 有了這些前提信息,我們來說下我遇到的問題,在這樣的背景下,我采用tomcat7-maven-plugin進(jìn)行啟動(dòng)測試

以下tomcat:run...命令為tomcat7-maven-plugin的命令,scope為javax.servlet-api包在maven中的scope。

  1. tomcat:run + scope=provided:正常啟動(dòng)
  2. tomcat:run + scope=compile:啟動(dòng)失敗
  3. tomcat:run-war + scope=provided:正常啟動(dòng)
  4. tomcat:run-war + scope=compile:正常啟動(dòng)
  • 詭異了吧,如果是2和4一起啟動(dòng)失敗,那我也沒什么探索的欲望了,合乎情理,雖然其中還有很多細(xì)節(jié)模棱兩可。
  • 另外提前貼下2報(bào)錯(cuò)的核心信息:
java.lang.ClassCastException: ch.qos.logback.classic.servlet.LogbackServletContainerInitializer cannot be cast to javax.servlet.ServletContainerInitializer
  • 可以明確LogbackServletContainerInitializer是實(shí)現(xiàn)了javax.servlet.ServletContainerInitializer接口的,這邊類型轉(zhuǎn)換失敗只有一個(gè)原因:類加載器不對(duì)?。?!

問題排查

先看看這兩個(gè)類的類加載器

寫個(gè)Servlet監(jiān)聽器,在啟動(dòng)時(shí)打出加載器和jar包信息

public class Callback implements ServletContextListener {

    public void doCallback() {
        System.out.println("查看看類加載器 ... ");
        System.out.println("LogbackServletContainerInitializer = " + LogbackServletContainerInitializer.class.getClassLoader());
        System.out.println("ServletContainerInitializer = " + ServletContainerInitializer.class.getClassLoader());
        
        System.out.println("查看加載類所在jar包路徑 ... ");
        System.out.println("LogbackServletContainerInitializer = " + LogbackServletContainerInitializer.class.getProtectionDomain().getCodeSource().getLocation());
        System.out.println("ServletContainerInitializer = " + ServletContainerInitializer.class.getProtectionDomain().getCodeSource().getLocation());
    }

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        doCallback();
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        doCallback();
    }
}

在web.xml中配置好,啟動(dòng),發(fā)現(xiàn):
tomcat:run + scope=compile啟動(dòng)失敗,無法打印出類加載信息。。。
tomcat:run + scope=provided
tomcat:run-war + scope=provided
tomcat:run-war + scope=compile
這三個(gè)的類加載信息是一致的,如下:

查看看類加載器 ... 
LogbackServletContainerInitializer = WebappClassLoader
  context: 
  delegate: false
  repositories:
----------> Parent Classloader:
ClassRealm[plugin>org.apache.tomcat.maven:tomcat7-maven-plugin:2.2, parent: sun.misc.Launcher$AppClassLoader@18b4aac2]

ServletContainerInitializer = ClassRealm[plugin>org.apache.tomcat.maven:tomcat7-maven-plugin:2.2, parent: sun.misc.Launcher$AppClassLoader@18b4aac2]
查看加載類所在jar包路徑 ... 
LogbackServletContainerInitializer = file:/Users/coselding/.m2/repository-weidian/ch/qos/logback/logback-classic/1.2.3/logback-classic-1.2.3.jar
ServletContainerInitializer = file:/Users/coselding/.m2/repository-weidian/org/apache/tomcat/embed/tomcat-embed-core/7.0.47/tomcat-embed-core-7.0.47.jar

Tomcat類加載器架構(gòu)

tomcat類加載器架構(gòu).jpg

結(jié)合Tomcat的類加載器架構(gòu),ServletContainerInitializer的類加載器ClassRealm應(yīng)該就是對(duì)應(yīng)的Common ClassLoader,而LogbackServletContainerInitializer就是WebappClassLoader,是Common ClassLoader的子加載器。和上面的場景結(jié)合起來就是,如果javax.servlet-api scope=compile,那么javax.servlet-api這個(gè)包就會(huì)在tomcat/lib下和應(yīng)用WEB-INF/lib下各有一份,加載器分別是Common ClassLoader和WebappClassLoader。
我們知道JavaEE的規(guī)范中在應(yīng)用間依賴隔離作了規(guī)定:***tomcat/lib下和應(yīng)用WEB-INF/lib如果有相同的依賴,WEB-INF/lib是優(yōu)先于tomcat/lib的,這個(gè)邏輯是為了支持tomcat部署多應(yīng)用時(shí)應(yīng)用間依賴隔離,打破了雙親委派原則 ***,如下圖:


WebAppClassLoader加載邏輯.jpg

因此你的WEB-INF/lib目錄下的javax.servlet-api會(huì)被會(huì)在LogbackServletContainerInitializer加載時(shí)加載WebappClassLoader,而Tomcat啟動(dòng)自己加載自己lib目錄下的那份WebappClassLoader,導(dǎo)致了ClassCastException。這個(gè)過程用圖示如下:


ServletContainerInitializer類加載.jpg

因此LogbackServletContainerInitializer實(shí)現(xiàn)的ServletContainerInitializer接口和tomcat識(shí)別的ServletContainerInitializer不是同一個(gè)類加載器加載的,故報(bào)錯(cuò)。

  • 到這里解決了scope=compile和scope=provided所造成的區(qū)別。
  • 但是很遺憾,場景2由于類加載失敗,程序直接無法啟動(dòng),我無法查看其類加載器的情況。

tomcat:run和tomcat:run-war的區(qū)別

我們用ServletContainerInitializer.class.getProtectionDomain().getCodeSource().getLocation()打出類加載所在jar包的路徑,來確認(rèn)下,tomcat:run-war加載的到底是哪個(gè)類,這段代碼由于是放在webapp中的,如果WEB-INF/lib目錄下存在javax.servlet-api的話應(yīng)該優(yōu)先加載的。

  1. 啟動(dòng)信息分析(tomcat:run-war + scope=compile)
查看加載類所在jar包路徑 ... 
LogbackServletContainerInitializer = file:/Users/coselding/Projects/vweex/test/target/test-1.0.0-SNAPSHOT/WEB-INF/lib/logback-classic-1.2.3.jar
ServletContainerInitializer = file:/Users/coselding/.m2/repository-weidian/org/apache/tomcat/embed/tomcat-embed-core/7.0.47/tomcat-embed-core-7.0.47.jar

加載的確實(shí)是tomcat內(nèi)部自帶的javax.servlet-api,那我們放在WEB-INF/lib下的javax.servlet-api被忽略了?答案是的?。。∥覀兛锤暾娜罩荆?/p>

七月 24, 2018 3:36:57 下午 org.apache.catalina.loader.WebappClassLoader validateJarFile
信息: validateJarFile(/Users/coselding/Projects/vweex/test/target/test-1.0.0-SNAPSHOT/WEB-INF/lib/javax.servlet-api-3.0.1.jar) - jar not loaded. See Servlet Spec 2.3, section 9.7.2. Offending class: javax/servlet/Servlet.class
查看加載類所在jar包路徑 ... 
LogbackServletContainerInitializer = file:/Users/coselding/Projects/vweex/test/target/test-1.0.0-SNAPSHOT/WEB-INF/lib/logback-classic-1.2.3.jar
ServletContainerInitializer = file:/Users/coselding/.m2/repository-weidian/org/apache/tomcat/embed/tomcat-embed-core/7.0.47/tomcat-embed-core-7.0.47.jar

看見了嗎?我們WEB-INF/lib目錄下的jar被忽略了,WebappClassLoader在加載時(shí)做了校驗(yàn),給出了警告,但是tomcat自己仍然會(huì)加載自身的javax.servlet-api,確保程序正常,這也是我們平時(shí)在項(xiàng)目中不太在意這個(gè)細(xì)節(jié),但是程序仍然能正確執(zhí)行的原因

  • 我們先區(qū)分下這兩種啟動(dòng)方式的差別:
  1. tomcat:run + scope=compile
    是以你的項(xiàng)目源文件目錄作為執(zhí)行目錄的,不會(huì)在target目錄下生成war文件,如下圖:


    tomcat-run目錄結(jié)構(gòu).png

他的好處是什么呢?這是一個(gè)開發(fā)時(shí)工具,你修改代碼會(huì)自動(dòng)進(jìn)行熱部署,避免每次改代碼都需要重新啟動(dòng)!那么我們可以了解下熱部署的原理:深入理解Java類加載器(2):線程上下文類加載器,這是為了開發(fā)方便而把類加載過程復(fù)雜化了,這個(gè)過程暫時(shí)不做了解,但是可以大致定位是這個(gè)復(fù)雜的類加載過程中有bug,導(dǎo)致了加載javax.servlet-api時(shí)沒像正式部署時(shí)WebAppClassLoader正確過濾。

  1. tomcat:run-war + scope=compile
    會(huì)先把你的項(xiàng)目打包成war,再啟動(dòng)tomcat容器加載這個(gè)war,所以tomcat:run-war方式和我們?cè)诎l(fā)布系統(tǒng)打包發(fā)布的流程是類似的,缺點(diǎn)是這種啟動(dòng)方式你更改代碼是不會(huì)運(yùn)行時(shí)生效的,需要重新啟動(dòng),因?yàn)榇a改動(dòng)不會(huì)影響target/{projectName}目錄下的文件,目錄結(jié)構(gòu)如下圖:


    tomcat-run-war目錄結(jié)構(gòu).png

解決方式

主要你保證你的項(xiàng)目依賴中mvn dependency:tree查到的所有servlet-api依賴都是provided,就能從根源上避免這個(gè)問題,這里有個(gè)坑:
Servlet2.0依賴坐標(biāo)

<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
</dependency>

Servlet3.0依賴坐標(biāo)

<dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
</dependency>

呵呵。。。我們的dubbo中這兩個(gè)包都依賴了,需要全部exclude。。。

參考

總結(jié)

在符合雙親委派原則的基礎(chǔ)上,我們通常不會(huì)遇到上述問題,那為什么要打破雙親委派原則呢?目前來看主要兩種情形:

  1. Tomcat遵循JavaEE標(biāo)準(zhǔn),需要支持多應(yīng)用部署時(shí)的依賴隔離問題,這就需要子加載器加載類優(yōu)先于父加載器,否則兩個(gè)不同的webapp如果依賴了兩個(gè)不同版本的Spring,可能就出問題了,也如上文所說,Tomcat特做了一些兼容,針對(duì)servlet-api等一些特殊的包進(jìn)行了過濾。
  2. SPI、JNDI等情形,接口定義在框架層(父加載器),但是實(shí)現(xiàn)類卻在應(yīng)用層jar(子加載器),框架啟動(dòng)時(shí)卻需要去掃描加載子加載器管理范疇內(nèi)的類,這種情況下采用線程上下文加載器來打破雙親委派原則,幫助實(shí)現(xiàn)框架層功能。
    因此如果你開發(fā)的是應(yīng)用層程序,這部分內(nèi)容通常不需要考慮,如果開發(fā)的是框架層程序,那用到類加載器時(shí)就要心存敬畏之心了!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,285評(píng)論 6 342
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,688評(píng)論 19 139
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,872評(píng)論 11 349
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong閱讀 22,966評(píng)論 1 92
  • 嘉陵江水長又長, 順著山腳恣意淌, 不怕前路多坎...
    朗園閱讀 408評(píng)論 0 0

友情鏈接更多精彩內(nèi)容