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

在通常的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節點的描述:apache

<Context docBase="test" path="/test" reloadable="true"/>

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

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

在 前面的文章 中曾經講過Tomcat7在啓動完成後會有一個後臺線程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方法中的這段代碼:tomcat

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

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

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方法:less

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方法同樣分析,再也不贅述。webapp

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

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

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

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

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

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


StandardContext實例數


WebappLoader實例數


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<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
                        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<String, String> env = new Hashtable<String, String>();
        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 <code>null</code>.
     *
     * @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;

    }

瞭解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()這部分,但兩個內置變量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中描述的須要與應用一塊兒加載的纔會當即加載,不然只有到該類首次使用時纔會由類加載器加載。

關於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方法的調用路徑

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

相關文章
相關標籤/搜索