問題
?流量
?微服務化讓我們的系統(tǒng)變得越來越多,往往在項目發(fā)布時,我們打出來的jar包或war包會非常的大,在利用CI&PI工具進行遠程發(fā)布時,帶寬可能會成為瓶頸。
?磁盤空間
?以springboot為例,當中間件運行多個springboot項目時,每個項目war包的WEB-INF/lib下都會有相同的一些springboot框架的jar包,對磁盤空間來說是浪費(當然這個影響是微不足道的)
思路
?我們可以考慮把依賴的第三方jar包抽離,事先放入tomcat中間件中,在持續(xù)發(fā)布時,我們僅需要把項目本身的代碼打成war包發(fā)布。當多個服務依賴的第三方jar包相同時,甚至可以將抽離的jar包作為共享。以springboot項目為例,springboot框架的jar包是每個springboot項目所依賴的,這時我們可以將springboot框架的jar包抽出來作為共享jar包,所有的springboot項目在打包時排除這些jar包,在中間件中發(fā)布時依賴共享jar包。
實現(xiàn)
?Tomcat的類加載機制為我們提供了shared.loader配置,可以讓同一個tomcat下運行的項目依賴一些共享的jar包。
以下講解以tomcat-9.0.16版本為例
- 在tomcat安裝目錄下創(chuàng)建share/lib文件夾,放入共享的jar包。
- 修改conf/catalina.properties配置
shared.loader="${catalina.base}/share/lib","${catalina.base}/share/lib/*.jar","${catalina.home}/share/lib","${catalina.home}/share/lib/*.jar" - 利用maven-war-plugins插件在打包時排除共享jar包,打war包放入tomcat/webapp下
- 啟動tomcat
很好,噩夢開始了
?大多數(shù)情況下以上使用是沒有問題的。這也是官方提供的用法,大家盡情使用。
?一開始我也是興高采烈地就這樣操作了,然后就遇到了下面的問題:
Bean named 'myEntiy' is expected to be of type 'org.liuhao.kuangjia.vo.MyEntiy' but was actually of type 'org.liuhao.kuangjia.vo.MyEntiy'
?當然還可能是下面這些錯誤:
java.lang.LinkageError: loader constraint violation: loader (instance of java/net/URLClassLoader) previously initiated loading for a different type with name xxx
org.liuhao.kuangjia.vo.MyEntiy Can not Cast to org.liuhao.kuangjia.vo.MyEntiy
?當時我就懵逼了,明明是按照官方教程來的,咋會出現(xiàn)這種莫名其妙的錯誤。
?出現(xiàn)以上問題的表面原因是因為我在項目中重寫了jar包中的MyEntiy類,導致JVM中加載了兩個相同的類。按理來說覆蓋jar包類是我們的常規(guī)操作,為什么會出現(xiàn)這種問題呢,這個就要涉及到tomcat設計的類加載機制問題了。
Tomcat類加載機制
?先來看一張圖

?如上圖所說,tomcat的中有以上6種類加載器。
?Bootstrap —— 這個類加載器包含Java虛擬機提供的基本運行時類,以及系統(tǒng)擴展目錄($JAVA_HOME/jre/lib/ext)中的JAR文件中的任何類。注意:一些JVM可能將其實現(xiàn)為多個類加載器,或者它可能根本不可見(作為類加載器)。
?System —— 這個類加載器負責加載以下幾個jar包
- $CATALINA_HOME/bin/bootstrap.jar
- $CATALINA_BASE/bin/tomcat-juli.jar or $CATALINA_HOME/bin/tomcat-juli.jar
- $CATALINA_HOME/bin/commons-daemon.jar
?Common —— 這個類加載器加載common.loader(conf/catalina.properties)下的classes文件、資源文件和jar包,這些類對Tomcat內(nèi)部類和所有Web應用程序都是可見的。
?Server —— 這個類加載器加載server.loader(conf/catalina.properties)下的classes文件、資源文件和jar包,只對Tomcat內(nèi)部可見,對web應用程序完全不可見。
?Shared —— 這個類加載器加載shared.loader(conf/catalina.properties)下的classes文件、資源文件和jar包,對所有web應用程序可見,并可用于在所有web應用程序之間共享代碼。但是,對此共享代碼的任何更新都需要Tomcat重新啟動
?Webapp —— 為部署在單個Tomcat實例中的每個Web應用程序創(chuàng)建的類加載器。加載Web應用程序的/Web-INF/Class目錄中的所有未打包類和資源,再加上Web應用程序/Web-INF/lib目錄下JAR文件中的類和資源,對此web應用程序都是可見的,而對其他類則不可見。
更詳細的介紹參考官網(wǎng)
?要找到上面問題的根本原因,首先我們得了解一個詞匯:雙親委派(即當類加載器在加載某個類時,先會把需要加載的類名交給父類加載器,如果父類加載器無法加載,才會由自身完成類加載。此模式是為了防止重復加載類以及惡意篡改核心類)
?眾所周知,JVM提供的類加載器都是遵循雙親委派的。值得注意的是,查看tomcat源碼后可以發(fā)現(xiàn)WebappClassLoader是破壞了雙親委派原則的,關(guān)鍵代碼如下
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// (2) Search local repositories
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
?變量delegate的默認值為false,所以從上面代碼我們可以發(fā)現(xiàn),WebappClassLoader在進行類加載時,會自身先嘗試加載所需的類,如果自身無法完成類加載,才會委托給父類加載器進行加載。
?到此,一切都真相大白了。因為我把項目依賴的jar包放入了shared.loader下,在容器初始化時,org.liuhao.kuangjia.vo.MyEntiy由于被Spring Ioc初始化,此時調(diào)用org.liuhao.kuangjia.vo.MyEntiy類的相關(guān)類是被SharedClassLoder加載的(因為相關(guān)jar包都在shared.loader下),由于類加載器的傳遞性,會讓SharedClassLoder加載jar包里的org.liuhao.kuangjia.vo.MyEntiy類,而由于我們項目中由重寫了org.liuhao.kuangjia.vo.MyEntiy類,在項目中調(diào)用org.liuhao.kuangjia.vo.MyEntiy類時,WebappClassLoader又會重新加載項目下的org.liuhao.kuangjia.vo.MyEntiy類,此時JVM會認為兩個類是不同的(因為由不同的類加載器加載)。繼而造成了上面我們提到的各種問題。
(我個人認為這算是一個BUG,這樣的設計使得需要share框架時幾乎無法滿足要求。當然更可能是我水平有限不會用)
?那我們在使用tomcat提供的共享類機制時,一定要覆蓋共享類怎么辦呢?以下兩個辦法:
- 在覆蓋共享類時,要注意將調(diào)用此類的相關(guān)類都做覆蓋(非常復雜繁瑣)
- 修改tomcat源碼,自己實現(xiàn)共享jar包功能(需要一些技術(shù)水平)
修改源碼,拓展WebappClassLoader
?終于進入正題了,領(lǐng)導提出了這種需求,你總不能跟他Bala一堆tomcat怎么怎么了,只能采用迂回戰(zhàn)術(shù),自己研究。
?首先整理一下戰(zhàn)略思路,問題出在SharedClassLoader和WebAppClassLoder的沖突上,那我們可以放棄使用SharedClassLoader,把共享類都讓給WebAppCLassLoder去加載。研究了一下類加載部分的源碼,發(fā)現(xiàn)可行!篇幅問題就不贅述我在這期間趟過的各種坑了,總之最后是成功的搞出來了,先看修改源碼后的使用方式:
- 將修改后的catatlina模塊源碼打包為
catalina.jar,替換tomcat/lib下的catalina.jar - 在linux下創(chuàng)建/server/share_customer/lib文件夾,放入共享的jar包。
- 修改
conf/catalina.properties配置,在share.loader=下,增加以下配置
liuhao.loader=/server/share_customer/lib - 利用maven-war-plugins插件在打包時排除共享jar包,打war包放入tomcat/webapp下
- 啟動tomcat。
源碼改動
1、StandardRoot類下增加方法
protected void processWebInfYinHaiLib() throws LifecycleException {
String value = CatalinaProperties.getProperty("liuhao.loader");
if ((value != null) && (!value.equals(""))) {
Arrays.stream(value.split(",")).forEach(path -> {
File file = new File(path);
if(file.isDirectory()){
Arrays.stream(file.listFiles()).filter(pathname -> pathname.getName().endsWith(".jar")).forEach(shareJar -> {
try {
log.info("自定義共享jar包加入classes:"+shareJar.getName());
createWebResourceSet(ResourceSetType.CLASSES_JAR,
"/WEB-INF/classes", shareJar.toURI().toURL(), "/");
} catch (MalformedURLException e) {
e.printStackTrace();
}
});
}
});
}
}
2、StandardRoot類修改startInternal方法
@Override
protected void startInternal() throws LifecycleException {
mainResources.clear();
main = createMainResourceSet();
mainResources.add(main);
for (List<WebResourceSet> list : allResources) {
// Skip class resources since they are started below
if (list != classResources) {
for (WebResourceSet webResourceSet : list) {
webResourceSet.start();
}
}
}
// This has to be called after the other resources have been started
// else it won't find all the matching resources
processWebInfLib();
//加入自定義共享包
processWebInfYinHaiLib();
// Need to start the newly found resources
for (WebResourceSet classResource : classResources) {
classResource.start();
}
cache.enforceObjectMaxSizeLimit();
setState(LifecycleState.STARTING);
}
3、修改WebappClassLoaderBase類的start()方法如下
/**
* Start the class loader.
*
* @exception LifecycleException if a lifecycle error occurs
*/
@Override
public void start() throws LifecycleException {
state = LifecycleState.STARTING_PREP;
WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
for (WebResource classes : classesResources) {
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()));
}
}
log.info("【裝載自定義jar包,增加classpath路徑】");
String value = CatalinaProperties.getProperty("liuhao.loader");
if ((value != null) && (!value.equals(""))) {
Arrays.stream(value.split(",")).forEach(path -> {
File file = new File(path);
if(file.isDirectory()){
Arrays.stream(file.listFiles()).filter(pathname -> pathname.getName().endsWith(".jar")).forEach(shareJar -> {
try {
localRepositories.add(shareJar.toURI().toURL());
} catch (MalformedURLException e) {
e.printStackTrace();
}
});
}
});
}
state = LifecycleState.STARTED;
}
?自此就完全實現(xiàn)了jar包共享的功能,由于都由同一類加載器加載,并且保證了加載順序,使得jar包類覆蓋的操作可以順利實現(xiàn)。