Tomcat 7 自動加載類及檢測文件變更原理

在通常的 web 應用開發裏一般會使用開發工具(如 Eclipse、IntelJ )集成 tomcat ,這樣能夠將 web 工程項目直接發佈到 tomcat 中,而後一鍵啓動。常常遇到的一種狀況是直接修改一個類的源文件,此時開發工具會直接將編譯後的 class 文件發佈到 tomcat 的 web 工程裏,但若是 tomcat 沒有配置應用的自動加載功能的話,當前 JVM 中運行的 class 仍是源文件修改以前編譯好的 class 文件。能夠重啓 tomcat 來加載新的 class 文件,但這樣作須要再手工點擊一次restart,爲了可以在應用中即時看到 java 文件修改以後的執行狀況,能夠在 tomcat 中將應用配置成自動加載模式,其配置很簡單,只要在配置文件的Context節點中加上一個 reloadable 屬性爲true便可,示例以下:java

<Context path="/HelloWorld" docBase="C:/apps/apache-tomcat/DeployedApps/HelloWorld" reloadable="true"/>
複製代碼

若是你的開發工具已經集成了 tomcat 的話應該會有一個操做界面配置來代替手工添加文件信息,如 Eclipse 中是以下界面來配置的: web

此時須要把 Auto reloading enabled前面的複選框鉤上。其背後的原理實際也是在 server.xml 文件中加上 Context 節點的描述:

<Context docBase="test" path="/test" reloadable="true"/>
複製代碼

這樣 Tomcat 就會監控所配置的 web 應用實際路徑下的/WEB-INF/classes/WEB-INF/lib兩個目錄下文件的變更,若是發生變動 tomcat 將會自動重啓該應用。apache

熟悉 Tomcat 的人應該都用過這個功能,就再也不詳述它的配置步驟了。我感興趣的是這個自動加載功能在 Tomcat 7 中是怎麼實現的。編程

前面的文章中曾經講過 Tomcat 7 在啓動完成後會有一個後臺線程ContainerBackgroundProcessor[StandardEngine[Catalina]],這個線程將會定時(默認爲 10 秒)執行 Engine、Host、Context、Wrapper 各容器組件及與它們相關的其它組件的 backgroundProcess 方法,這段代碼在全部容器組件的父類org.apache.catalina.core.ContainerBase類的 backgroundProcess`方法中:數組

public void backgroundProcess() {
    
    if (!getState().isAvailable())
        return;

    if (cluster != null) {
        try {
            cluster.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e);                
        }
    }
    if (loader != null) {
        try {
            loader.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e);                
        }
    }
    if (manager != null) {
        try {
            manager.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.manager", manager), e);                
        }
    }
    Realm realm = getRealmInternal();
    if (realm != null) {
        try {
            realm.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e);                
        }
    }
    Valve current = pipeline.getFirst();
    while (current != null) {
        try {
            current.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e);                
        }
        current = current.getNext();
    }
    fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}
複製代碼

與自動加載類相關的代碼在 loader 的 backgroundProcess 方法的調用時。每個 StandardContext 會關聯一個loader變量,該變量的初始化在org.apache.catalina.core.StandardContext類的 startInternal 方法中的這段代碼:緩存

if (getLoader() == null) {
    WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
    webappLoader.setDelegate(getDelegate());
    setLoader(webappLoader);
}
複製代碼

因此上面的 loader.backgroundProcess() 方法的調用將會執行org.apache.catalina.loader.WebappLoader類的 backgroundProcess 方法:tomcat

public void backgroundProcess() {
    if (reloadable && modified()) {
        try {
            Thread.currentThread().setContextClassLoader
                (WebappLoader.class.getClassLoader());
            if (container instanceof StandardContext) {
                ((StandardContext) container).reload();
            }
        } finally {
            if (container.getLoader() != null) {
                Thread.currentThread().setContextClassLoader
                    (container.getLoader().getClassLoader());
            }
        }
    } else {
        closeJARs(false);
    }
}
複製代碼

其中reloadable變量的值就是本文開始提到的配置文件的 Context 節點的reloadable屬性的值,當它爲true而且 modified() 方法返回也是true時就會執行 StandardContext 的 reload 方法:bash

public synchronized void reload() {

    // Validate our current component state
    if (!getState().isAvailable())
        throw new IllegalStateException
            (sm.getString("standardContext.notStarted", getName()));

    if(log.isInfoEnabled())
        log.info(sm.getString("standardContext.reloadingStarted",
                getName()));

    // Stop accepting requests temporarily.
    setPaused(true);

    try {
        stop();
    } catch (LifecycleException e) {
        log.error(
            sm.getString("standardContext.stoppingContext", getName()), e);
    }

    try {
        start();
    } catch (LifecycleException e) {
        log.error(
            sm.getString("standardContext.startingContext", getName()), e);
    }

    setPaused(false);

    if(log.isInfoEnabled())
        log.info(sm.getString("standardContext.reloadingCompleted",
                getName()));

}
複製代碼

reload 方法中將先執行 stop 方法將原有的該 web 應用停掉,再調用 start 方法啓動該 Context ,start 方法的分析前文已經說過,stop 方法能夠參照 start 方法同樣分析,再也不贅述。app

這裏重點要說的是上面提到的監控文件變更的方法 modified ,只有它返回true纔會致使應用自動加載。看下該方法的實現:less

public boolean modified() {
    return classLoader != null ? classLoader.modified() : false ;
}
複製代碼

能夠看到這裏面實際調用的是 WebappLoader 的實例變量 classLoader 的 modified 方法來判斷的,下文就詳細分析這個 modified 方法的實現。

先簡要說一下 Tomcat 中的加載器。在 Tomcat 7 中每個 web 應用對應一個 Context 節點,這個節點在 JVM 中就對應一個org.apache.catalina.core.StandardContext對象,而每個 StandardContext 對象內部都有一個加載器實例變量(即其父類org.apache.catalina.core.ContainerBaseloader實例變量),前面已經看到這個 loader 變量其實是org.apache.catalina.loader.WebappLoader對象。而每個 WebappLoader 對象內部關聯了一個 classLoader 變量(就這這個類的定義中,能夠看到該變量的類型是org.apache.catalina.loader.WebappClassLoader)。

在 Tomcat 7 的源碼中給出了 6 個 web 應用:

因此在 Tomcat 啓動完成以後理論上應該有 6 個 StandardContext 對象,6 個 WebappLoader 對象,6 個 WebappClassLoader 對象。用 jvisualvm 觀察實際狀況也證明了上面的判斷:

StandardContext 實例數:

StandardContext 實例數

WebappLoader 實例數:

WebappLoader 實例數

WebappClassLoader 實例數

WebappClassLoader 實例數

上面講過了 WebappLoader 的初始化代碼,接下來說一下 WebappClassLoader 的對象初始化代碼。一樣仍是在 StandardContext 類的 startInternal 方法中,有以下兩段代碼:

if (getLoader() == null) {
    WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
    webappLoader.setDelegate(getDelegate());
    setLoader(webappLoader);
}
複製代碼

這一段是上面已經說過的 WebappLoader 的初始化。

try {

    if (ok) {
        
        // Start our subordinate components, if any
        if ((loader != null) && (loader instanceof Lifecycle))
            ((Lifecycle) loader).start();
複製代碼

這一段與 WebappLoader 的對象相關,執行的就是 WebappLoader 類的 start 方法,由於 WebappLoader 繼承自 LifecycleBase 類,因此調用它的 start 方法最終將會執行該類自定義的 startInternal 方法,看下 startInternal 方法中的這段代碼:

classLoader = createClassLoader();
classLoader.setResources(container.getResources());
classLoader.setDelegate(this.delegate);
classLoader.setSearchExternalFirst(searchExternalFirst);
if (container instanceof StandardContext) {
    classLoader.setAntiJARLocking(
            ((StandardContext) container).getAntiJARLocking());
    classLoader.setClearReferencesStatic(
            ((StandardContext) container).getClearReferencesStatic());
    classLoader.setClearReferencesStopThreads(
            ((StandardContext) container).getClearReferencesStopThreads());
    classLoader.setClearReferencesStopTimerThreads(
            ((StandardContext) container).getClearReferencesStopTimerThreads());
    classLoader.setClearReferencesHttpClientKeepAliveThread(
            ((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
}

for (int i = 0; i < repositories.length; i++) {
    classLoader.addRepository(repositories[i]);
}

// Configure our repositories
setRepositories();
setClassPath();

setPermissions();

((Lifecycle) classLoader).start();
複製代碼

一開始調用了 createClassLoader 方法:

/**
 * Create associated classLoader.
 */
private WebappClassLoader createClassLoader()
    throws Exception {

    Class clazz = Class.forName(loaderClass);
    WebappClassLoader classLoader = null;

    if (parentClassLoader == null) {
        parentClassLoader = container.getParentClassLoader();
    }
    Class[] argTypes = { ClassLoader.class };
    Object[] args = { parentClassLoader };
    Constructor constr = clazz.getConstructor(argTypes);
    classLoader = (WebappClassLoader) constr.newInstance(args);

    return classLoader;

}
複製代碼

能夠看出這裏經過反射實例化了一個 WebappClassLoader 對象。

回到文中上面提的問題,看下 WebappClassLoader 的 modified 方法代碼:

/**
 * Have one or more classes or resources been modified so that a reload
 * is appropriate?
 */
public boolean modified() {

    if (log.isDebugEnabled())
        log.debug("modified()");

    // Checking for modified loaded resources
    int length = paths.length;

    // A rare race condition can occur in the updates of the two arrays
    // It's totally ok if the latest class added is not checked (it will
    // be checked the next time
    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]) {
                if( log.isDebugEnabled() )
                    log.debug("  Resource '" + paths[i]
                              + "' was modified; Date is now: "
                              + new java.util.Date(lastModified) + " Was: "
                              + new java.util.Date(lastModifiedDates[i]));
                return (true);
            }
        } catch (NamingException e) {
            log.error("    Resource '" + paths[i] + "' is missing");
            return (true);
        }
    }

    length = jarNames.length;

    // Check if JARs have been added or removed
    if (getJarPath() != null) {

        try {
            NamingEnumeration 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
                    log.info("    Additional JARs have been added : '"
                             + name + "'");
                    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) {
            if (log.isDebugEnabled())
                log.debug("    Failed tracking modifications of '"
                    + getJarPath() + "'");
        } catch (ClassCastException e) {
            log.error("    Failed tracking modifications of '"
                      + getJarPath() + "' : " + e.getMessage());
        }

    }

    // No classes have been modified
    return (false);

}
複製代碼

這段代碼從整體上看共分紅兩部分,第一部分檢查 web 應用中的 class 文件是否有變更,根據 class 文件的最近修改時間來比較,若是有不一樣則直接返回true,若是 class 文件被刪除也返回true。第二部分檢查 web 應用中的 jar 文件是否有變更,若是有一樣返回true。稍有編程經驗的人對於以上比較代碼都容易理解,但對這些變量的值,特別是裏面比較時常常用到 WebappClassLoader 類的實例變量的值是在什麼地方賦值的會比較困惑,這裏就這點作一下說明。

以 class 文件變更的比較爲例,比較的關鍵代碼是:

long lastModified =
                    ((ResourceAttributes) resources.getAttributes(paths[i]))
                    .getLastModified();
                if (lastModified != lastModifiedDates[i]) {
複製代碼

即從 WebappClassLoader 的實例變量resources中取出文件當前的最近修改時間,與 WebappClassLoader 原來緩存的該文件的最近修改時間作比較。

關於 resources.getAttributes 方法,看下 resources 的聲明類型javax.naming.directory.DirContext可知實際這裏面執行的是一般的 JNDI 查詢一個屬性的方法(若是對 JNDI 不熟悉請看一下 JNDI 的相關文檔大體瞭解一下,這裏再也不作單獨介紹),因此有必要把 resources 變量到底是何對象拎出來講一下。

在上面看 WebappLoader 的 startInternal 方法的源碼裏 createClassLoader() 方法調用並賦值給 classLoader 下一行:

classLoader.setResources(container.getResources());
複製代碼

這裏設置的 resources 就是上面用到的 resources 變量,能夠看到它實際是 WebappLoader 所關聯容器的實例變量 resources 。按前面的描述所關聯的容器即 StandardContext ,再來看看 StandardContext 中 resources 是怎麼賦值的。

仍是在 StandardContext 的 startInternal 方法中,開頭部分有這段代碼:

// Add missing components as necessary
if (webappResources == null) {   // (1) Required by Loader
    if (log.isDebugEnabled())
        log.debug("Configuring default Resources");
    try {
        if ((getDocBase() != null) && (getDocBase().endsWith(".war")) &&
                (!(new File(getBasePath())).isDirectory()))
            setResources(new WARDirContext());
        else
            setResources(new FileDirContext());
    } catch (IllegalArgumentException e) {
        log.error("Error initializing resources: " + e.getMessage());
        ok = false;
    }
}
if (ok) {
    if (!resourcesStart()) {
        log.error( "Error in resourceStart()");
        ok = false;
    }
}
複製代碼

由於默認的應用是否是 war 包發佈,而是以目錄形式發佈的因此會執行setResources(new FileDirContext())方法。這裏稍微曲折的地方是 setResources 裏實際只是給 StandardContext 的 webappResources 變量賦值,而 StandardContext 的 resources 變量賦爲null,在上面源碼中的最後 resourcesStart 方法的調用中才會給 resources 賦值。看下 resourcesStart 方法:

public boolean resourcesStart() {

    boolean ok = true;

    Hashtable env = new Hashtable();
    if (getParent() != null)
        env.put(ProxyDirContext.HOST, getParent().getName());
    env.put(ProxyDirContext.CONTEXT, getName());

    try {
        ProxyDirContext proxyDirContext =
            new ProxyDirContext(env, webappResources);
        if (webappResources instanceof FileDirContext) {
            filesystemBased = true;
            ((FileDirContext) webappResources).setAllowLinking
                (isAllowLinking());
        }
        if (webappResources instanceof BaseDirContext) {
            ((BaseDirContext) webappResources).setDocBase(getBasePath());
            ((BaseDirContext) webappResources).setCached
                (isCachingAllowed());
            ((BaseDirContext) webappResources).setCacheTTL(getCacheTTL());
            ((BaseDirContext) webappResources).setCacheMaxSize
                (getCacheMaxSize());
            ((BaseDirContext) webappResources).allocate();
            // Alias support
            ((BaseDirContext) webappResources).setAliases(getAliases());
            
            if (effectiveMajorVersion >=3 && addWebinfClassesResources) {
                try {
                    DirContext webInfCtx =
                        (DirContext) webappResources.lookup(
                                "/WEB-INF/classes");
                    // Do the lookup to make sure it exists
                    webInfCtx.lookup("META-INF/resources");
                    ((BaseDirContext) webappResources).addAltDirContext(
                            webInfCtx);
                } catch (NamingException e) {
                    // Doesn't exist - ignore and carry on
                }
            }
        }
        // Register the cache in JMX
        if (isCachingAllowed()) {
            String contextName = getName();
            if (!contextName.startsWith("/")) {
                contextName = "/" + contextName;
            }
            ObjectName resourcesName = 
                new ObjectName(this.getDomain() + ":type=Cache,host=" 
                               + getHostname() + ",context=" + contextName);
            Registry.getRegistry(null, null).registerComponent
                (proxyDirContext.getCache(), resourcesName, null);
        }
        this.resources = proxyDirContext;
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.error(sm.getString("standardContext.resourcesStart"), t);
        ok = false;
    }

    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 方法的調用,在 BaseDirContext 類中全部的 getAttributes 方法最終都會調用抽象方法 doGetAttributes 來返回查詢屬性的結果,這個方法在 FileDirContext 的定義以下:

protected Attributes doGetAttributes(String name, String[] attrIds)
    throws NamingException {

    // Building attribute list
    File file = file(name);

    if (file == null)
        return null;

    return new FileResourceAttributes(file);

}
複製代碼

能夠看到內部執行了 file 方法:

/**
 * Return a File object representing the specified normalized
 * context-relative path if it exists and is readable.  Otherwise,
 * return null複製代碼.
 *
 * @param name Normalized context-relative path (with leading '/')
 */
protected File file(String name) {

    File file = new File(base, name);
    if (file.exists() && file.canRead()) {

        if (allowLinking)
            return file;
        
        // Check that this file belongs to our root path
        String canPath = null;
        try {
            canPath = file.getCanonicalPath();
        } catch (IOException e) {
            // Ignore
        }
        if (canPath == null)
            return null;

        // Check to see if going outside of the web application root
        if (!canPath.startsWith(absoluteBase)) {
            return null;
        }

        // Case sensitivity check - this is now always done
        String fileAbsPath = file.getAbsolutePath();
        if (fileAbsPath.endsWith("."))
            fileAbsPath = fileAbsPath + "/";
        String absPath = normalize(fileAbsPath);
        canPath = normalize(canPath);
        if ((absoluteBase.length() < absPath.length())
            && (absoluteBase.length() < canPath.length())) {
            absPath = absPath.substring(absoluteBase.length() + 1);
            if (absPath == null)
                return null;
            if (absPath.equals(""))
                absPath = "/";
            canPath = canPath.substring(absoluteBase.length() + 1);
            if (canPath.equals(""))
                canPath = "/";
            if (!canPath.equals(absPath))
                return null;
        }

    } else {
        return null;
    }
    return file;

}
複製代碼null複製代碼

瞭解 java 的文件操做的人這段代碼就很容易理解了,實際就是根據傳入的文件名查找目錄下是否存在該文件,若是存在則返回包裝了的文件屬性對象 FileResourceAttributes 。 FileResourceAttributes 類實際是對java.io.File類作了一層包裝,如 getLastModified 方法實際調用的是 File 類的 lastModified 方法返回:

long lastModified =
                    ((ResourceAttributes) resources.getAttributes(paths[i]))
                    .getLastModified();
                if (lastModified != lastModifiedDates[i]) {
複製代碼

以上分析了上面這段代碼中((ResourceAttributes) resources.getAttributes(paths[i])).getLastModified()這部分,但兩個內置變量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 中描述的須要與應用一塊兒加載的纔會當即加載,不然只有到該類首次使用時纔會由類加載器加載。

關於 Tomcat 的自定義類加載器是一個頗有意思的話題,可說的地方不少,後面會專文另述。而關於 jar 包文件變更的比較代碼同 class 文件比較的相似,一樣是取出當前 web 應用WEB-INF\lib目錄下的全部 jar 文件,與 WebappClassLoader 內部緩存的jarNames數組作比較,若是文件名不一樣或新加或刪除了 jar 文件都返回true

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

if ((jarPath != null) && (jar.startsWith(jarPath))) {

    String jarName = jar.substring(jarPath.length());
    while (jarName.startsWith("/"))
        jarName = jarName.substring(1);

    String[] result = new String[jarNames.length + 1];
    for (i = 0; i < jarNames.length; i++) {
        result[i] = jarNames[i];
    }
    result[jarNames.length] = jarName;
    jarNames = result;

}
複製代碼

而 addJar 方法是在 WebappLoader 類的 startInternal 方法中,上面已經給出與這個相關的代碼,裏面的這段代碼部分:

// Configure our repositories
setRepositories();
setClassPath();
複製代碼

在 setRepositories 的方法最後部分:

try {
    JarFile jarFile = new JarFile(destFile);
    classLoader.addJar(filename, jarFile, destFile);
} catch (Exception ex) {
    // Catch the exception if there is an empty jar file
    // Should ignore and continue loading other jar files
    // in the dir
}

loaderRepositories.add( filename );
複製代碼

即在 tomcat 啓動時的加載web應用的過程裏就會加載該應用的 lib 目錄下的全部 jar 文件,同時給 WebappClassLoader 的實例變量 jarNames 添加數組元素。

addJar 方法的調用路徑:

addJar 方法的調用路徑

在看 jar 包加載的代碼時會不斷碰到 resources 對象 list、getAttributes 等方法的調用,記住這裏實際上調用的是上面提到的 FileDirContext 的相關方法,也即對於文件的查詢訪問方法就清楚了。

相關文章
相關標籤/搜索