Tomcat 7 自动加载类及检测文件变动原理
在一般的 web 應用開發里通常會使用開發工具(如 Eclipse、IntelJ )集成 tomcat ,這樣可以將 web 工程項目直接發布到 tomcat 中,然后一鍵啟動。經常遇到的一種情況是直接修改一個類的源文件,此時開發工具會直接將編譯后的 class 文件發布到 tomcat 的 web 工程里,但如果 tomcat 沒有配置應用的自動加載功能的話,當前 JVM 中運行的 class 還是源文件修改之前編譯好的 class 文件。可以重啟 tomcat 來加載新的 class 文件,但這樣做需要再手工點擊一次restart,為了能夠在應用中即時看到 java 文件修改之后的執行情況,可以在 tomcat 中將應用配置成自動加載模式,其配置很簡單,只要在配置文件的Context節點中加上一個 reloadable 屬性為true即可,示例如下:
<Context path="/HelloWorld" docBase="C:/apps/apache-tomcat/DeployedApps/HelloWorld" reloadable="true"/> 復制代碼如果你的開發工具已經集成了 tomcat 的話應該會有一個操作界面配置來代替手工添加文件信息,如 Eclipse 中是如下界面來配置的:
此時需要把Auto reloading enabled前面的復選框鉤上。其背后的原理實際也是在 server.xml 文件中加上 Context 節點的描述: <Context docBase="test" path="/test" reloadable="true"/> 復制代碼這樣 Tomcat 就會監控所配置的 web 應用實際路徑下的/WEB-INF/classes和/WEB-INF/lib兩個目錄下文件的變動,如果發生變更 tomcat 將會自動重啟該應用。
熟悉 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 方法:
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 方法:
public synchronized void reload() {// Validate our current component stateif (!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 方法一樣分析,不再贅述。
這里重點要說的是上面提到的監控文件變動的方法 modified ,只有它返回true才會導致應用自動加載。看下該方法的實現:
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.ContainerBase的loader實例變量),前面已經看到這個 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 實例數:
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 anyif ((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 resourcesint 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 timeint 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 removedif (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 folderif (!name.endsWith(".jar"))continue;if (!name.equals(jarNames[i])) {// Missing JARlog.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 allowedif (name.endsWith(".jar")) {// There was more JARslog.info(" Additional JARs have been added");return (true);}}} else if (i < jarNames.length) {// There was less JARslog.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 modifiedreturn (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 Loaderif (log.isDebugEnabled())log.debug("Configuring default Resources");try {if ((getDocBase() != null) && (getDocBase().endsWith(".war")) &&(!(new File(getBasePath())).isDirectory()))setResources(new WARDirContext());elsesetResources(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 existswebInfCtx.lookup("META-INF/resources");((BaseDirContext) webappResources).addAltDirContext(webInfCtx);} catch (NamingException e) {// Doesn't exist - ignore and carry on}}}// Register the cache in JMXif (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 listFile 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 pathString 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 rootif (!canPath.startsWith(absoluteBase)) {return null;}// Case sensitivity check - this is now always doneString 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 的相關方法,也即對于文件的查詢訪問方法就清楚了。
總結
以上是生活随笔為你收集整理的Tomcat 7 自动加载类及检测文件变动原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ionic使用CardIO实现扫描银行卡
- 下一篇: Rancher 2.0 里程碑版本:支持