Tomcat 源碼分析(三)-(三)-自動加載類及檢測文件變動原理

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類做了一層包裝。

關於已加載類的資源信息

還有兩個內置變量pathslastModifiedDates值究竟什麼時候賦的呢?

說一下 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 的實例變量lastModifiedDatespaths數組添加元素。**這裏就解答了上面提到的文件變更比較代碼的疑問。要說明的是在 tomcat 啓動後 web 應用中所有的 class 文件並不是全部加載的,而是配置在 web.xml 中描述的需要與應用一起加載的纔會立即加載,否則只有到該類首次使用時纔會由類加載器加載。

而關於 jar 包文件變動的比較代碼同 class 文件比較的類似,同樣是取出當前 web 應用WEB-INF\lib目錄下的所有 jar 文件,與 WebappClassLoader 內部緩存的jarNames數組做比較,如果文件名不同或新加或刪除了 jar 文件都返回true

這裏 jarNames 變量的初始賦值代碼在 WebappClassLoader 類的 addJar 方法中的開頭部分…

最後這一點點,看不下去了 (╯‵□′)╯︵┻━┻

結束

2019-05-16 小杭


參考資料


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章