Tomcat 源碼分析(三)-WEB加載原理(三)
Tomcat 7 自動加載類及檢測文件變動原理
關於開發工具中的自動加載
在常用的web應用開發工具(如 Eclipse、IntelJ )中都有集成Tomcat,這樣可以將開發的web項目直接發佈到tomcat中去。這裏會遇到一種情況,在修改了一個文件後,開發工具可以直接編譯class文件發佈到tomcat的web工程裏面。如果tomcat沒有配置自動加載功能,JVM中的還是就的class,就需要手動進行restart。
所以,這裏說一下tomcat提供的配置自動加載的配置屬性:
`<Context path="/HelloWorld" docBase="C:/apps/apache-tomcat/DeployedApps/HelloWorld" reloadable="true"/>`
就是reloadable="true"
這個屬性,這樣 Tomcat 就會監控所配置的 web 應用實際路徑下的/WEB-INF/classes
和/WEB-INF/lib
兩個目錄下文件的變動,如果發生變更 tomcat 將會自動重啓該應用。
分析Tomcat自動加載的實現
自動加載的實現,先從Tomcat在啓動之後會有一個後臺線程,
ContainerBackgroundProcessor[StandardEngine[Catalina]]
定時【默認10秒】執行Engine、Host、Context、Wrapper 各容器組件及與它們相關的其它組件的 backgroundProcess 方法。- 這裏開始分析。
這個方法被定義在,所有容器組件的父類org.apache.catalina.core.ContainerBase
類的 backgroundProcess`方法中:
/**
*執行定期任務,如重新加載等。此方法將
*在該容器的類加載上下文中調用。意外的
*丟棄物將被捕獲並記錄。
*/
@Override
public void backgroundProcess() {
if (!getState().isAvailable())
return;
Cluster cluster = getClusterInternal();
if (cluster != null) {
try {
cluster.backgroundProcess();
} catch (Exception e) {
log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e);
}
}
//刪減後的↓↓↓↓↓↓↓ 逐個調用內部相關的backgroundProcess()方法
Loader loader = getLoaderInternal();
loader.backgroundProcess();
//**********現在要看看的是上面↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑這個的地方了*******
Manager manager = getManagerInternal();
manager.backgroundProcess();
Realm realm = getRealmInternal();
realm.backgroundProcess();
//調用管道內左右閥的backgroundProcess()方法
Valve current = pipeline.getFirst();
while (current != null) {
current.backgroundProcess();
current = current.getNext();
}
//最後這裏註冊了一個Lifecycle.PERIODIC_EVENT事件 之前分析加載web是在這個事件的處理中
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}
這裏與自動加載的代碼是Loader :Loader loader = getLoaderInternal();
,loader.backgroundProcess();
這兩段。
這裏看一下這個loader 變量是什麼時候初始化的:【在StandardContext的startInternal 方法中】
if (getLoader() == null) { WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); }
這裏可以看到這裏這個設置的Loader 的類是WebappLoader。
然後具體的關聯是在Loader的backgroundProcess()中:
//public class WebappLoader extends LifecycleMBeanBase implements Loader, PropertyChangeListener {
@Override
public void backgroundProcess() {
if (reloadable && modified()) {
try {
Thread.currentThread().setContextClassLoader
(WebappLoader.class.getClassLoader());
if (container instanceof StandardContext) {
((StandardContext) container).reload();
}
} finally {......}
} else {
closeJARs(false);
}
}
這裏可以看到,這裏的條件是reloadable和modified(),這裏的reloadable就是配置Context節點的reloadable屬性值,而modified()這個方法是對檢查文件變動的,之後會分析。
先來看一下,最終要執行的重新加載的方法:StandardContext類的reload():
public synchronized void reload() {
......
// Stop accepting requests temporarily.
setPaused(true);
try {
stop();
} catch (LifecycleException e) {.......}
try {
start();
} catch (LifecycleException e) {....... }
setPaused(false);
......
}
這裏的reload方法中,將執行stop方法將原有的該 web 應用停掉,再調用 start 方法啓動該 Context 。
start方法,則會重新加載啓動web應用。【就像之前分析的那樣_(:з」∠)_】
檢測文件變動分析
前面,進行reload重新啓動web應用的條件爲:if (reloadable && modified()) {
,一個爲配置值,另一個就是接下來要說的了。- modified()
//public class WebappLoader extends LifecycleMBeanBase implements Loader, PropertyChangeListener {
public boolean modified() {
return classLoader != null ? classLoader.modified() : false ;
}
這裏進行判斷的的實際方法是:WebappLoader 的實例變量 classLoader 的 modified 方法。
說明個Tomcat中加載器的東東,每個web應用會對一個Context節點,在JVM中就會對應一個
org.apache.catalina.core.StandardContext
對象,而每一個StandardContext對象內部都一個加載器實例loader實例變量。可以看到前面說明,這個loader實際上是WebappLoader對象。而每一個 WebappLoader 對象內部關聯了一個 classLoader 變量(就這這個類的定義中,可以看到該變量的類型是
org.apache.catalina.loader.WebappClassLoader
)。所以,這裏一個web應用會對應一個StandardContext 一個WebappLoader 一個WebappClassLoader 。
WebappLoader 的初始化
WebappLoader的初始化在StandardContext 的初始化的時候已經完成了。上文中已有了:
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
......
if ((loader != null) && (loader instanceof Lifecycle))
((Lifecycle) loader).start();
這裏要的代碼是先初始化了,之後執行了loader的start()方法,因爲WebappLoader 本身也是繼承了LifecycleBase 類,所以這裏的start()方法,最終也會執行到類自定義的startInternal 方法。
WebappLoader.startInternal ()方法的源碼:
//public class WebappLoader extends LifecycleMBeanBase implements Loader, PropertyChangeListener {
@Override
protected void startInternal() throws LifecycleException {
......
// 爲JNDI協議註冊流處理程序工廠 ?? 啥意思啊 ╮(╯_╰)╭
// Register a stream handler factory for the JNDI protocol
URLStreamHandlerFactory streamHandlerFactory =
DirContextURLStreamHandlerFactory.getInstance();
......
URL.setURLStreamHandlerFactory(streamHandlerFactory);
......
}
// ********基於當前存儲庫列表構造類加載器*********需要看的就是這一段
// Construct a class loader based on our current repositories list
try {
classLoader = createClassLoader(); // 開始就調用了這個創建加載器的方法
classLoader.setJarOpenInterval(this.jarOpenInterval);
classLoader.setResources(container.getResources());
classLoader.setDelegate(this.delegate);
classLoader.setSearchExternalFirst(searchExternalFirst);
if (container instanceof StandardContext) {
classLoader.setAntiJARLocking(
((StandardContext) container).getAntiJARLocking());
classLoader.setClearReferencesRmiTargets(
((StandardContext) container).getClearReferencesRmiTargets());
classLoader.setClearReferencesStatic(
((StandardContext) container).getClearReferencesStatic());
classLoader.setClearReferencesStopThreads(
((StandardContext) container).getClearReferencesStopThreads());
classLoader.setClearReferencesStopTimerThreads(
((StandardContext) container).getClearReferencesStopTimerThreads());
classLoader.setClearReferencesHttpClientKeepAliveThread(
((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
classLoader.setClearReferencesObjectStreamClassCaches(
((StandardContext) container).getClearReferencesObjectStreamClassCaches());
}
for (int i = 0; i < repositories.length; i++) {
classLoader.addRepository(repositories[i]);
}
// Configure our repositories
setRepositories();
setClassPath();
setPermissions();
((Lifecycle) classLoader).start();
// Binding the Webapp class loader to the directory context
.....
} catch (Throwable t) {......}
setState(LifecycleState.STARTING);
}
/**
* Create associated classLoader. 創建關聯的類加載器。
* 這裏反射實例化了一個WebappClassLoader 對象。
*/
private WebappClassLoaderBase createClassLoader()
throws Exception {
Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null;
if (parentClassLoader == null) {
parentClassLoader = container.getParentClassLoader();
}
Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args);
return classLoader;
}
這裏,就分析了這個要使用的類的初始化過程了。
WebappClassLoader 的 modified 方法-檢測變動的代碼
可以再前邊看到,判斷文件變動的檢測代碼爲modified()方法:
classLoader != null ? classLoader.modified() : false ;
就是這句代碼,所以來看一下這個**classLoader.modified()**也就是WebappClassLoader 的:
public boolean modified() {
......s
// Checking for modified loaded resources
int length = paths.length;
int length2 = lastModifiedDates.length;
if (length > length2)
length = length2;
//****這裏對比資源文件裏面的文件的最後修改時間是否一致,以便判斷是否變動****
for (int i = 0; i < length; i++) {
try {
long lastModified =
((ResourceAttributes) resources.getAttributes(paths[i]))
.getLastModified();
if (lastModified != lastModifiedDates[i]) {
......
return (true);
}
} catch (NamingException e) {......return (true);}
}
length = jarNames.length;
// Check if JARs have been added or removed
if (getJarPath() != null) {
try {
NamingEnumeration<Binding> enumeration =
resources.listBindings(getJarPath());
int i = 0;
while (enumeration.hasMoreElements() && (i < length)) {
NameClassPair ncPair = enumeration.nextElement();
String name = ncPair.getName();
// Ignore non JARs present in the lib folder
if (!name.endsWith(".jar"))
continue;
if (!name.equals(jarNames[i])) {
// Missing JAR
......
return (true);
}
i++;
}
if (enumeration.hasMoreElements()) {
while (enumeration.hasMoreElements()) {
NameClassPair ncPair = enumeration.nextElement();
String name = ncPair.getName();
// Additional non-JAR files are allowed
if (name.endsWith(".jar")) {
// There was more JARs
log.info(" Additional JARs have been added");
return (true);
}
}
} else if (i < jarNames.length) {
// There was less JARs
log.info(" Additional JARs have been added");
return (true);
}
} catch (NamingException e) {.......}
}
// No classes have been modified
return (false);
}
這段代碼從總體上看共分成兩部分,第一部分檢查 web 應用中的 class 文件是否有變動,根據 class 文件的最近修改時間來比較,如果有不同則直接返回true
,如果 class 文件被刪除也返回true
。
第二部分檢查 web 應用中的 jar 文件是否有變動,如果有同樣返回true
。
這裏的代碼看起來,還是比較容易理解的╮(╯_╰)╭
關於當前資源信息獲取
關於,檢查文件變動的關鍵代碼就是:
long lastModified =
((ResourceAttributes) resources.getAttributes(paths[i]))
.getLastModified();
if (lastModified != lastModifiedDates[i]) {
WebappClassLoader 的實例變量resources
中取出文件當前的最近修改時間,與 WebappClassLoader 原來緩存的該文件的最近修改時間做比較。
這裏看一下 resources.getAttributes 方法:
這裏的resources實際上的是javax.naming.directory.DirContext
類,看下初始化的地方,在WebappLoader 的 startInternal 方法中:【就在上面的】
classLoader.setResources(container.getResources()); //這裏設置的,是在StandardContext初始化的時候
((Lifecycle) classLoader).start();
**StandardContext 中 resources 是怎麼賦值:**StandardContext 的 startInternal 方法中
// Add missing components as necessary
if (webappResources == null) { // (1) Required by Loader
try {
if ((getDocBase() != null) && (getDocBase().endsWith(".war")) &&
(!(new File(getBasePath())).isDirectory()))
setResources(new WARDirContext()); //我們常用的wer發佈加載的是這個
else
setResources(new FileDirContext()); //默認的應用是文件發佈的
} catch (IllegalArgumentException e) {......ok = false;}
}
if (ok) {
if (!resourcesStart()) {...... } //在這裏做了初始化
}
這裏會對resources進行賦值,並且初始化;看下resourcesStart()初始化的方法:
//public class StandardContext extends ContainerBase
public boolean resourcesStart() {
......
try {
ProxyDirContext proxyDirContext =
new ProxyDirContext(env, webappResources);
......// 中間的太多不知道啥的東西 (ノ`Д)ノ
super.setResources(proxyDirContext); //要看的就只是這個
} catch (Throwable t) {......}
return (ok);
}
很明顯,這裏的resources 賦的是 proxyDirContext 對象,而 proxyDirContext 是一個代理對象,代理的就是 webappResources ,按上面的描述即org.apache.naming.resources.FileDirContext
。
org.apache.naming.resources.FileDirContext
繼承自抽象父類org.apache.naming.resources.BaseDirContext
,而 BaseDirContext 又實現了javax.naming.directory.DirContext
接口。所以 JNDI 操作中的 lookup、bind、getAttributes、rebind、search 等方法都已經在這兩個類中實現了。當然裏面還有 JNDI 規範之外的方法如 list 等。
所以,接下來看看一下這個getAttributes 方法的調用。
最終都會調用到抽象方法 doGetAttributes 的。
//public abstract class BaseDirContext implements DirContext { public final Attributes getAttributes(String name, String[] attrIds) throws NamingException { ...... // Next do a standard lookup Attributes attrs = doGetAttributes(name, attrIds);
看一下FileDirContext 的doGetAttributes定義:
protected Attributes doGetAttributes(String name, String[] attrIds)
throws NamingException {
// Building attribute list
File file = file(name, true);
if (file == null) return null;
return new FileResourceAttributes(file);
}
到這裏就可以了,最終是調用了File的東西【java文件操作】。
實際就是根據傳入的文件名查找目錄下是否存在該文件,如果存在則返回包裝了的文件屬性對象 FileResourceAttributes 。 FileResourceAttributes 類實際是對java.io.File
類做了一層包裝。
關於已加載類的資源信息
還有兩個內置變量paths
和lastModifiedDates
值究竟什麼時候賦的呢?
說一下 WebappClassLoader 這個自定義類加載器的用法,在 Tomcat 中所有 web 應用內
WEB-INF\classes
目錄下的 class 文件都是用這個類加載器來加載的,一般的自定義加載器都是覆寫 ClassLoader 的 findClass 方法,這裏也不例外。WebappClassLoader 覆蓋的是 URLClassLoader 類的 findClass 方法,而在這個方法內部最終會調用findResourceInternal(String name, String path)
方法:// Register the full path for modification checking // Note: Only syncing on a 'constant' object is needed synchronized (allPermission) { int j; long[] result2 = new long[lastModifiedDates.length + 1]; for (j = 0; j < lastModifiedDates.length; j++) { result2[j] = lastModifiedDates[j]; } result2[lastModifiedDates.length] = entry.lastModified; lastModifiedDates = result2; String[] result = new String[paths.length + 1]; for (j = 0; j < paths.length; j++) { result[j] = paths[j]; } result[paths.length] = fullPath; paths = result; }
這裏可以看到在**加載一個新的 class 文件時會給 WebappClassLoader 的實例變量
lastModifiedDates
和paths
數組添加元素。**這裏就解答了上面提到的文件變更比較代碼的疑問。要說明的是在 tomcat 啓動後 web 應用中所有的 class 文件並不是全部加載的,而是配置在 web.xml 中描述的需要與應用一起加載的纔會立即加載,否則只有到該類首次使用時纔會由類加載器加載。
而關於 jar 包文件變動的比較代碼同 class 文件比較的類似,同樣是取出當前 web 應用WEB-INF\lib
目錄下的所有 jar 文件,與 WebappClassLoader 內部緩存的jarNames
數組做比較,如果文件名不同或新加或刪除了 jar 文件都返回true
。
這裏 jarNames 變量的初始賦值代碼在 WebappClassLoader 類的 addJar 方法中的開頭部分…
最後這一點點,看不下去了 (╯‵□′)╯︵┻━┻
結束
2019-05-16 小杭