前幾日使用 Jetty (9.2)部署公司一個 web 項目,這個項目原本部署在 Tomcat server上,一切正常,可是部署到 Jetty 后,啟動報錯.關鍵錯誤信息為"java.lang.NoClassDefFoundError: Could not initialize class org.apache.tomcat.jdbc.pool.DataSource"
項目使用了 Tomcat jdbc connection pool 當中有兩個 jar 包 tomcat-jdbc.jar 和 tomcat-juli.jar, 后者是前者的 maven 依賴,后者也是 tomcat 的日志抽象層,tomcat server自帶這個 jar 在 tomcat_home 的 bin 文件夾下.依據異常信息推斷錯誤是因為載入和初始化org.apache.tomcat.jdbc.pool.DataSource導致的,為了更easy分析問題,我創建了一個簡單的 web 項目僅僅依賴于這兩個 jar 包.部署到同一個 Jetty server中,報錯"java.util.ServiceConfigurationError: org.apache.juli.logging.Log: Provider org.eclipse.jetty.apache.jsp.JuliLog not a subtype"
查看源碼org.eclipse.jetty.apache.jsp.JuliLog非常明白是org.apache.juli.logging.Log的子類,但為什么會報這種錯誤呢,結合之前的java.lang.NoClassDefFoundError和java.util.ServiceConfigurationError能夠確定問題是因為類載入引起的,依據對類載入的了解,同一個類被不同的類載入器實例載入得到的 Class 對象是不同的.所以我判斷可能是因為 Jetty server使用了不同的類載入器實例載入了兩個累,導致繼承關系不存在了.查看java.util.ServiceConfigurationError相關的API文檔發現,這個 Error 是在 ServiceLoader載入 Service Provider 時發生的.查看 ServiceLoader 的源碼發現這樣一段
private S nextService() {if (!hasNextService())throw new NoSuchElementException();String cn = nextName;nextName = null;Class<?
> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider " + cn + " not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider " + cn + " not a subtype");//我遇到的錯誤信息剛好是這里. } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen }
為了驗證我的猜測,參考這段代碼我寫了個小程序來測試.主要代碼例如以下
public class Main {public static void main(String[] args) throws ClassNotFoundException {final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();final CustomClassLoader customClassLoader= new CustomClassLoader("target/classes", "自己定義載入器", systemClassLoader);//第一個參數的路徑是類的編譯路徑//使用系統類載入器載入 Child//由于 Child 依賴 Parent 所以系統類載入器會自己主動載入 Parent,這個行為與 jetty 的 WebAppClassCloader 相同Class<?> child = Class.forName("Child", true, systemClassLoader);//由于之前載入過,不會反復載入直接返回 Parent 類實例Class<?> parent = Class.forName("Child", true, systemClassLoader);//使用自己定義載入器載入 Child//自己定義載入器會優先嘗試自己載入,失敗后使用父載入器Class<?
> customChild = Class.forName("Child", true, customClassLoader); Class<?
> customParent = Class.forName("Parent", true, customClassLoader);//相同不會反復載入 //測試 //相同使用系統載入器載入的兩個類,繼承關系正常 System.out.println("parent.isAssignableFrom(child) = " + parent.isAssignableFrom(child));//true //使用自己定義載入器載入的 Child 卻不是系統類載入器載入的 Parent 的子類 System.out.println("parent.isAssignableFrom(customChild) = " + parent.isAssignableFrom(customChild));//false //相同使用自己定義載入器載入的兩個類,集成關系正常 System.out.println("customParent.isAssignableFrom(customChild) = " + customParent.isAssignableFrom(customChild));//true } }
這段代碼中 Child 是 Parent 的子類.這個簡單的測試驗證了我的猜測.查看文檔發現 Jettty 在9.2版本號中的 jsp 引擎使用的是 tomcat 的.在 Jetty 的 lib 里面能夠發現例如以下jar
org.eclipse.jetty.apache-jsp-9.2.3.v20140905.jar
org.eclipse.jetty.orbit.org.eclipse.jdt.core-3.8.2.v20130121.jar
org.mortbay.jasper.apache-el-8.0.9.M3.jar
org.mortbay.jasper.apache-jsp-8.0.9.M3.jar
org.mortbay開頭的兩個 jar 里面是 apache 的 jsp 實現類當中包括org.apache.juli.logging這個包,org.eclipse.jetty.apache-jsp-9.2.3.v20140905.jar這個 jar 中提供了 logging 的詳細實現,終于通過 ServiceLoader 載入.問題就出在這里,由于我的項目中有 tomcat-juli.jar 當中也包括org.apache.juli.logging這個包.
查看了一下 Jetty 的文檔中有關類載入的內容,發現 Jetty 對每一個部署的 web 應用使用單獨的WebAppClassLoader實例進行類載入.通常實現自己定義類載入器的時候會優先托付給父載入器(一般為系統類載入器,能夠通過 ClassLoader.getSystemClassLoader() 得到),然后再嘗試自己載入類.但 Jetty 的這個 WebAppClassLoader 正相反,除了對于系統類和server類(什么是系統類和server類能夠查看文檔),會優先嘗試自己載入,然后才托付父載入器.
依據這個行為基本能夠確認了,server載入 jsp 引擎是會使用自己的類載入器載入server lib 中上述的類(org.apache.juli.logging.Log及事實上現org.eclipse.jetty.apache.jsp.JuliLog),應用部署時會使用WebAppClassLoader載入應用 lib 中的org.apache.juli.logging.Log.載入過程是這種, WebAppClassLoader實例載入org.apache.tomcat.jdbc.pool.DataSource,其依賴org.apache.juli.logging.Log,優先嘗試自己載入,所以會從應用的 lib 中載入到這個類,而嘗試載入事實上現的時候發現應用 lib 中沒有,再托付給父類載入器,也就是 Jetty server的載入器,成功載入到org.eclipse.jetty.apache.jsp.JuliLog,這樣就是使用兩個不同的載入器實例載入了子類和父類,依據之前的測試結果,兩個類之間的繼承關系是不成立的.所以導致發生錯誤.
清楚了問題的解決辦法,怎么解決呢?
方案一,在 maven 配置中將 tomcat-juli 的依賴 scope 改為 provided,Jetty server已經提供了. 這樣在WebAppClassLoader 嘗試自己載入org.eclipse.jetty.apache.jsp.JuliLog時會失敗,進而托付父類載入器,這樣org.apache.juli.logging.Log及事實上現org.eclipse.jetty.apache.jsp.JuliLog兩個類就是同一個載入器載入了.
方案二,更改 WebAppClassLoader 的父類載入器的優先級,使其優先使用父類載入器.詳細配置方式能夠參考文檔.目標是調用?setParentLoaderPriority(true)
使用這兩個方案,相同能夠解決原始項目中的問題,可是為什么測試用的簡單 web 項目和原始項目的錯誤信息不同呢?
回到原始項目的錯誤信息發現,"java.lang.NoClassDefFoundError: Could not initialize class org.apache.tomcat.jdbc.pool.DataSource"這個錯誤是因為在載入類的時候無法初始化,那么看org.apache.tomcat.jdbc.pool.DataSource中,在類載入后要做的初始化操作有什么,通過查看源代碼發現private static final Log log = LogFactory.getLog(DataSource.class);僅僅有這句代碼是須要在類載入后進行的初始化,跟蹤這個語句發現終于會進入上文中提到的nextService方法,所以根源錯誤依舊是上面描寫敘述的.
至此問題得到比較圓滿的解釋和解決.
總結:
本文涉及到的知識點
1.Java虛擬機的類載入機制
2.JavaServiceProvider 載入機制
3.Java 類的初始化過程
4.Jetty server的配置方式