在通常的 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.ContainerBase
的loader
實例變量),前面已經看到這個 loader 變量其實是org.apache.catalina.loader.WebappLoader
對象。而每個 WebappLoader 對象內部關聯了一個 classLoader 變量(就這這個類的定義中,能夠看到該變量的類型是org.apache.catalina.loader.WebappClassLoader
)。
在 Tomcat 7 的源碼中給出了 6 個 web 應用:
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 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()
這部分,但兩個內置變量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 的相關方法,也即對於文件的查詢訪問方法就清楚了。