在通常的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的相關方法,也即對於文件的查詢訪問方法就清楚了。