Tomcat類加載器及應用間class隔離與共享
Tomcat的用戶一定都使用過其應用部署功能,無論是直接拷貝文件到webapps目錄,還是修改server.xml以目錄的形式部署,或者是增加虛擬主機,指定新的appBase等等。
但部署應用時,不知道你是否曾注意過這幾點:
-
如果在一個Tomcat內部署多個應用,甚至多個應用內使用了某個類似的幾個不同版本,但它們之間卻互不影響。這是如何做到的。
-
如果多個應用都用到了某類似的相同版本,是否可以統一提供,不在各個應用內分別提供,占用內存呢。
-
還有時候,在開發Web應用時,在pom.xml中添加了servlet-api的依賴,那實際應用的class加載時,會加載你的servlet-api 這個jar嗎
以上提到的這幾點,在Tomcat以及各類的應用服務器中,都是通過類加載器( ClasssLoader )來實現的。通過本文,你可以了解到Tomcat內部提供的各種類加載器,Web應用的class和資源等加載的方式,以及其內部的實現原理。在遇到類似問題時,更胸有成竹。
類加載器
Java語言本身,以及現在其它的一些基于JVM之上的語言(Groovy,Jython, Scala...),都是在將代碼編譯生成class文件,以實現跨多平臺,write once, run anywhere。最終的這些class文件,在應用中,又被加載到JVM虛擬機中,開始工作。而把class文件加載到JVM的組件,就是我們所說的類加載器。而對于類加載器的抽象,能面對更多的class數據提供形式,例如網絡、文件系統等。
Java中常見的那個 ClassNotFoundException 和 NoClassDefFoundError 就是類加載器告訴我們的。
Servlet規范指出,容器用于加載Web應用內Servlet的class loader, 允許加載位于Web應用內的資源。但不允許重寫java.*, javax.*以及容器實現的類。同時每個應用內使用 Thread.currentThread.getContextClassLoader() 獲得的類加載器,都是該應用區別于其它應用的類加載器等等。
根據Servlet規范,各個應用服務器廠商自行實現。所以像其他的一些應用服務器一樣, Tomcat也 提供了多種的類加載器 ,以便應用服務器內的class以及部署的Web應用類文件運行在容器中時,可以使用不同的class repositories。
在Java中,類加載器是以一種父子關系樹來組織的。除Bootstrap外,都會包含一個parent 類加載器。(這里寫parent 類加載器,而不是父類加載器,不是為了裝X,是為了避免和Java里的 父類 混淆) 一般以類加載器需要加載一個class或者資源文件的時候,他會先委托給他的parent類加載器,讓parent類加載器先來加載,如果沒有,才再在自己的路徑上加載 。這就是人們常說的雙親委托,即把類加載的請求委托給parent。
但是...,這里需要注意一下
對于Web應用的類加載,和上面的雙親委托是有區別的。
在Tomcat中,涉及到的類加載器大致有以下幾類,像官方文檔里這張表示一樣,這里對于Bootstrap和System這種加載Java基礎類的我們不做分析,主要來看一下后面的Common和WebappX這兩類class loader。
Bootstrap
|
System
|
Common
/ \
Webapp1 Webapp2 ...
Webapp類加載器
正如上面內容所說,Webapp類加載器,相對于傳統的Java的類加載器,最主要的區別是
子優先(child first)
也就是說,在Web應用內,需要加載一個類的時候,不是先委托給parent,而是先自己加載,在自己的類路徑上找不到才會再委托parent。
但是此處的子優先有些地方需要注意的是,Java的基礎類不允許其重新加載,以及servlet-api也不允許重新加載。
那為什么要先child之后再parent呢?我們前面說是Servlet規范規定的。但確實也是實際需要。假如我們兩個應用內使用了相同命名空間里的一個class,一個使用的是Spring 2.x,一個使用的是Spring 3.x。如果是parent先加載的話,在第一個應用加載后,第二個應用再需要的時候,就直接從parent里拿到了,但是卻不符合需要。
另外一點是,各個Web應用的類加載器,是相互獨立的,即WebappClassloader的多個實例,只有這樣,多個應用之間才可能使用不同版本的相同命令空間下的類庫,而不互相受影響。
該類加載器會加載Web應用的WEB-INF/classes內的class和資源文件,以及WEB-INF/lib下的所有jar文件。
當然,有些時候,有需要還按照傳統的Java類加載器加載class時,Tomcat內提供了配置,可以實現父優先。
Common 類加載器
通過上面的class loader組織的圖,可以知道Common 類加載器,是做為webapp類加載器的parent存在的。它是在以下文件中進行配置的:
TOMCAT_HOME/conf/catalina.properties
文檔中給的樣例:
對于目錄結尾的,視為class文件的加載路徑,對于目錄/*.jar結尾的,則視為目錄下所有jar會被加載。
這個配置,默認已經包含了Tomcat的base下的lib目錄和home下的lib目錄。
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
所以,lib目錄下的class和jar文件,在啟動時就都被加載了。
一般來說,這個類加載器用來加載一些既需要Tomcat容器內和所有應用共同可見的class,應用的class不建議放到這兒加載。
介紹完這兩個加載器之后,我們來看文章開始時提到的幾個問題:
- 多個應用之間類庫不互相沖突,是由于使用了不同的類加載器進行加載的。彼此之間如同路人。即使看起來同樣一個類,使用不同的類加載器加載,也是不同的對象,這點要引起注意。
- 多個應用之間,如果大家使用了相同的類庫,而且數據眾多,為了避免重復加載占用內存,就可以用到我們的Common 類加載器。只要在配置中指定對應的目錄,然后提取出共用的文件即可。我在之前的公司開發應用服務器時,就有客戶有這樣的需求。
- 對于我們應用內提供的Servlet-api,其實應用服務器是不會加載的,因為容器已經自已加載過了。當然,這里不是因為父優先還是子優先的問題,而是這類內容,是不允許被重寫的。如果你應用內有一個叫javax.servlet.Servlet的class,那加載后可能就影響了應用內的正常運行了。
我們看在Tomcat6.x中加載一個包含servlet 3.x api的jar,會直接提示jar not loaded.
類加載器實現分析
在Tomcat啟動時,會創建一系列的類加載器,在其主類Bootstrap的初始化過程中,會先初始化classloader,然后將其綁定到Thread中。
public void init() throws Exception {
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
SecurityClassLoad.securityClassLoad(catalinaLoader); }
其中initClassLoaders方法,會根據catalina.properties的配置,創建相應的classloader。由于默認只配置了common.loader屬性,所以其中只會創建一個出來
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
所以,后面線程中綁定的都一直是commonClassLoader。
然后,當一個應用啟動的時候,會為其創建對應的WebappClassLoader。此時會將commonClassLoader設置為其parent。下面的代碼是StandardContext類在啟動時創建WebappLoader的代碼
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
這里的getParentClassLoader會從當前組件的classLoader一直向上,找parent classLoader設置。之后注意下一行代碼
webappLoader. setDelegate
這就是在設置后面Web應用的類查找時是父優先還是子優先。這個配置可以在server.xml里,對Context組件進行配置。
即在Context元素下可以嵌套一個 Loader 元素,配置Loader的delegate即可,其默認為false,即子優先。類似于這樣
<Context>
<Loader className="" delegate ="true"/>
</Context>
注意Loader還有一個屬性是reloadable,用于表明對于/WEB-INF/classes/ 和 /WEB-INF/lib 下資源發生變化時,是否重新加載應用。這個特性在開發的時候,還是很有用的。
如果你的應用并沒有配置這個屬性,想要重新加載一個應用,只需要使用manager里的reload功能就可以。
有點跑題,回到我們說的delgate上面來,配置之后,可以指定Web應用類加載時,到底是使用父優先還是子優先。
這里的WebappLoader,就開始了正式的創建WebappClassLoader
private WebappClassLoaderBase createClassLoader()
throws Exception {
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
parentClassLoader = context.getParentClassLoader();
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
配置等信息使用前面Loader內的配置。
應用的classLoader也配置好之后,我們再來看真正應用需要class的時候,是如何子優先的。
在loadClass的時候,會調用到WebappClassLoader的loadClass方法,此時,查找一個class的步驟總結這樣幾步:
這里把方法中分步的注釋拿來羅列一下,
-
(0) Check our previously loaded local class cache
-
(0.1) Check our previously loaded class cache
-
(0.2) Try loading the class with the system class loader, to prevent
the webapp from overriding Java SE classes. This implements SRV.10.7.2
-
然后,會判斷是否啟用了securityManager,啟用時會進行packageAccess的檢查。
主要判斷已加載的類里是否已經包含,然后避免Java SE的classes被覆蓋,packageAccess的檢查。
之后,開始了我們的父優先子優先的流程。這里判斷是否使用delegate時,對于一些容器提供的class,也會跳過。
boolean delegateLoad = delegate || filter (name);
這里的filter就用來過濾容器提供的類以及servlet-api的類。
protected synchronized boolean filter(String name) {
if (name == null)
return false;
// Looking up the package
String packageName = null;
int pos = name.lastIndexOf('.');
if (pos != -1)
packageName = name.substring(0, pos);
else
return false;
packageTriggersPermit.reset(packageName);
if (packageTriggersPermit.lookingAt()) {
return false;
}
然后確定到底是父優先,還是子優先,開始類的加載
父優先
// (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
}
}
此時如果沒找到,就走到下面的代碼,開始查找本地的資源庫(repository)和子優先時一樣:
// (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
}
如果父優先和子優先都沒能查找到需要的class,此時會拋出
throw new ClassNotFoundException(name);
關于上面代碼,有一個地方,感興趣的同學可以再深入了解下,
clazz = Class.forName(name, false, parent);
也許你這么多年一直直接用Class.forName,沒管過后面還可以多傳兩個參數。
來自:https://zhuanlan.zhihu.com/p/24168200