我们知道tomcat启动 会带动我们自己的应用工程运行,有时候我们的应用工程却是以war形式存在的,那么tomcat 是怎么解析war工程,下面就是我对tomcat源码解析war工程的一些见解
1,我们知道tomcat有自己的一套运行周期,他的运行周期如图,
而 tomcat运行中,则通过LifecycleSupport类添加各种监听器 其中这些监听器以实现LifecycleListener接口的子类 下图是LifecyceListener的一些子类
我在这里重点讲述HostConfig 和ContextConfig
HostConfig对war工程前期的校验 而ContextConfig对war包的真正解析
LifecycleListener 代码如下
package org.apache.catalina; public interface LifecycleListener { public void lifecycleEvent(LifecycleEvent event); }
2, HostConfig中lifecycleEvent方法如下
public void lifecycleEvent(LifecycleEvent event) { // Identify the host we are associated with try { host = (Host) event.getLifecycle(); if (host instanceof StandardHost) { setCopyXML(((StandardHost) host).isCopyXML()); setDeployXML(((StandardHost) host).isDeployXML()); setUnpackWARs(((StandardHost) host).isUnpackWARs()); setContextClass(((StandardHost) host).getContextClass()); } } catch (ClassCastException e) { log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e); return; } // Process the event that has occurred if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) { check(); } else if (event.getType().equals(Lifecycle.START_EVENT)) { start(); } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { stop(); } }此方法中有三个状态 PERIODIC_EVENT START_EVENT STOP_EVENT 而我们知道war工程的解析不可能出现在结束状态 所以只能出现在 PERIODIC_EVENT START_EVENT 即check和start方法 下面请看两方法
protected void check() { if (host.getAutoDeploy()) { // Check for resources modification to trigger redeployment DeployedApplication[] apps = deployed.values().toArray(new DeployedApplication[0]); for (int i = 0; i < apps.length; i++) { if (!isServiced(apps[i].name)) checkResources(apps[i]); } // Check for old versions of applications that can now be undeployed if (host.getUndeployOldVersions()) { checkUndeploy(); } // Hotdeploy applications deployApps(); } } public void start() { if (log.isDebugEnabled()) log.debug(sm.getString("hostConfig.start")); try { ObjectName hostON = host.getObjectName(); oname = new ObjectName (hostON.getDomain() + ":type=Deployer,host=" + host.getName()); Registry.getRegistry(null, null).registerComponent (this, oname, this.getClass().getName()); } catch (Exception e) { log.error(sm.getString("hostConfig.jmx.register", oname), e); } if (host.getCreateDirs()) { //创建目录<host>中appBase属性下的目录(webapps)和conf/engine/host-name/ 这个目录个 File[] dirs = new File[] {appBase(),configBase()}; for (int i=0; i<dirs.length; i++) { if (!dirs[i].mkdirs() && !dirs[i].isDirectory()) { log.error(sm.getString("hostConfig.createDirs",dirs[i])); } } } if (!appBase().isDirectory()) { log.error(sm.getString( "hostConfig.appBase", host.getName(), appBase().getPath())); host.setDeployOnStartup(false); host.setAutoDeploy(false); } if (host.getDeployOnStartup()) deployApps(); }有两个方法可知他们其实最终运行到同一方法上 deployApps方法 代码如下
protected void deployApps() { //appBase属性是Server.xml中host节点的appBase属性 默认是webapps下 File appBase = appBase(); //F:\source\TOMCAT_7_0_57\output\build\conf\Catalina\localhost File configBase = configBase(); //通过appBase.list()可以罗列出host所对应的appBase属性所指的目录下工程 //过滤appBase属性所指目录下的工程 String[] filteredAppPaths = filterAppPaths(appBase.list()); // Deploy XML descriptors from configBase //添加部署conf/Catalina/hostName/Context.xml这个context这个 deployDescriptors(configBase, configBase.list()); // Deploy WARs deployWARs(appBase, filteredAppPaths); // Deploy expanded folders deployDirectories(appBase, filteredAppPaths); } /** * Deploy WAR files. */ protected void deployWARs(File appBase, String[] files) { if (files == null) return; ExecutorService es = host.getStartStopExecutor(); List<Future<?>> results = new ArrayList<Future<?>>(); for (int i = 0; i < files.length; i++) { /** * 排除webapps 下WEB-INF 和META-INF这种的文件夹 */ if (files[i].equalsIgnoreCase("META-INF")) continue; if (files[i].equalsIgnoreCase("WEB-INF")) continue; File war = new File(appBase, files[i]); //查看webapps下是否有.war结束的文件 if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") && war.isFile() && !invalidWars.contains(files[i]) ) { ContextName cn = new ContextName(files[i], true); if (isServiced(cn.getName())) { continue; } //简单校验一下war工程 if (deploymentExists(cn.getName())) { DeployedApplication app = deployed.get(cn.getName()); if (!unpackWARs && app != null) { // Need to check for a directory that should not be // there File dir = new File(appBase, cn.getBaseName()); if (dir.exists()) { if (!app.loggedDirWarning) { log.warn(sm.getString( "hostConfig.deployWar.hiddenDir", dir.getAbsoluteFile(), war.getAbsoluteFile())); app.loggedDirWarning = true; } } else { app.loggedDirWarning = false; } } continue; } // Check for WARs with /../ /./ or similar sequences in the name if (!validateContextPath(appBase, cn.getBaseName())) { log.error(sm.getString( "hostConfig.illegalWarName", files[i])); invalidWars.add(files[i]); continue; } //执行DeployWar中的run方法 results.add(es.submit(new DeployWar(this, cn, war))); } } for (Future<?> result : results) { try { result.get(); } catch (Exception e) { log.error(sm.getString( "hostConfig.deployWar.threaded.error"), e); } } }最终解析war工程 到了 DeployWar(this, cn, war))这个类中的run方法中
private static class DeployWar implements Runnable { private HostConfig config; private ContextName cn; private File war; public DeployWar(HostConfig config, ContextName cn, File war) { this.config = config; this.cn = cn; this.war = war; } @Override public void run() { //执行HostConfig类中的deyloyWar config.deployWAR(cn, war); } }又回到 HostConfig的deployWAR方法中
/** * @param cn * @param war * 解压war包 * 最终真正解压war包的却是ContextConfig */ protected void deployWAR(ContextName cn, File war) { // Checking for a nested /META-INF/context.xml JarFile jar = null; InputStream istream = null; FileOutputStream fos = null; BufferedOutputStream ostream = null; //检查War工程下是否有此文件 File xml = new File(appBase(), cn.getBaseName() + "/META-INF/context.xml"); boolean xmlInWar = false; JarEntry entry = null; /** * 以jar的形式访问War工程 */ try { jar = new JarFile(war); entry = jar.getJarEntry(Constants.ApplicationContextXml); if (entry != null) { xmlInWar = true; } } catch (IOException e) { /* Ignore */ } finally { entry = null; if (jar != null) { try { jar.close(); } catch (IOException ioe) { // Ignore; } jar = null; } } Context context = null; //解析Context.xml文件中Context节点生成Context接口的类 /** * 正常情况生成StandardContext类 但是也可能出现失败情况 则生成FailedContext类 */ try { if (deployXML && xml.exists() && !copyXML) { synchronized (digesterLock) { try { context = (Context) digester.parse(xml); } catch (Exception e) { log.error(sm.getString( "hostConfig.deployDescriptor.error", war.getAbsolutePath()), e); } finally { if (context == null) { context = new FailedContext(); } digester.reset(); } } context.setConfigFile(xml.toURI().toURL()); } else if (deployXML && xmlInWar) { synchronized (digesterLock) { try { jar = new JarFile(war); entry = jar.getJarEntry(Constants.ApplicationContextXml); istream = jar.getInputStream(entry); context = (Context) digester.parse(istream); } catch (Exception e) { log.error(sm.getString( "hostConfig.deployDescriptor.error", war.getAbsolutePath()), e); } finally { if (context == null) { context = new FailedContext(); } context.setConfigFile(new URL("jar:" + war.toURI().toString() + "!/" + Constants.ApplicationContextXml)); if (istream != null) { try { istream.close(); } catch (IOException e) { /* Ignore */ } istream = null; } entry = null; if (jar != null) { try { jar.close(); } catch (IOException e) { /* Ignore */ } jar = null; } digester.reset(); } } } else if (!deployXML && xmlInWar) { // Block deployment as META-INF/context.xml may contain security // configuration necessary for a secure deployment. log.error(sm.getString("hostConfig.deployDescriptor.blocked", cn.getPath(), Constants.ApplicationContextXml, new File(configBase(), cn.getBaseName() + ".xml"))); } else { context = (Context) Class.forName(contextClass).newInstance(); } } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t); } finally { if (context == null) { context = new FailedContext(); } } boolean copyThisXml = false; if (deployXML) { if (host instanceof StandardHost) { copyThisXml = ((StandardHost) host).isCopyXML(); } // If Host is using default value Context can override it. if (!copyThisXml && context instanceof StandardContext) { copyThisXml = ((StandardContext) context).getCopyXML(); } if (xmlInWar && copyThisXml) { // Change location of XML file to config base xml = new File(configBase(), cn.getBaseName() + ".xml"); entry = null; try { jar = new JarFile(war); entry = jar.getJarEntry(Constants.ApplicationContextXml); istream = jar.getInputStream(entry); fos = new FileOutputStream(xml); ostream = new BufferedOutputStream(fos, 1024); byte buffer[] = new byte[1024]; while (true) { int n = istream.read(buffer); if (n < 0) { break; } ostream.write(buffer, 0, n); } ostream.flush(); } catch (IOException e) { /* Ignore */ } finally { if (ostream != null) { try { ostream.close(); } catch (IOException ioe) { // Ignore } ostream = null; } if (fos != null) { try { fos.close(); } catch (IOException ioe) { // Ignore } fos = null; } if (istream != null) { try { istream.close(); } catch (IOException ioe) { // Ignore } istream = null; } if (jar != null) { try { jar.close(); } catch (IOException ioe) { // Ignore; } jar = null; } } } } DeployedApplication deployedApp = new DeployedApplication(cn.getName(), xml.exists() && deployXML && copyThisXml); long startTime = 0; // Deploy the application in this WAR file //开始解压war包 并且交给ContextConfig //因为Context 才是真正的应用节点动 Host只是在这里校验 检测路径等 xml文件预设是否完善的等工作的了解 if(log.isInfoEnabled()) { startTime = System.currentTimeMillis(); log.info(sm.getString("hostConfig.deployWar", war.getAbsolutePath())); } try { // Populate redeploy resources with the WAR file deployedApp.redeployResources.put (war.getAbsolutePath(), Long.valueOf(war.lastModified())); if (deployXML && xml.exists() && copyThisXml) { deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); } else { // In case an XML file is added to the config base later deployedApp.redeployResources.put( (new File(configBase(), cn.getBaseName() + ".xml")).getAbsolutePath(), Long.valueOf(0)); } //class org.apache.catalina.startup.ContextConfig //添加ContextConfig这个监听器 Class<?> clazz = Class.forName(host.getConfigClass()); LifecycleListener listener = (LifecycleListener) clazz.newInstance(); context.addLifecycleListener(listener); context.setName(cn.getName()); context.setPath(cn.getPath()); context.setWebappVersion(cn.getVersion()); context.setDocBase(cn.getBaseName() + ".war"); //往Host节点中添加Context节点(从conf/server.xml 我们可知Context是host的一个子节点 这里正常情况下StandardContext) host.addChild(context); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error(sm.getString("hostConfig.deployWar.error", war.getAbsolutePath()), t); } finally { // If we're unpacking WARs, the docBase will be mutated after // starting the context if (unpackWARs && context != null && context.getDocBase() != null) { File docBase = new File(appBase(), cn.getBaseName()); deployedApp.redeployResources.put(docBase.getAbsolutePath(), Long.valueOf(docBase.lastModified())); addWatchedResources(deployedApp, docBase.getAbsolutePath(), context); if (deployXML && !copyThisXml && (xmlInWar || xml.exists())) { deployedApp.redeployResources.put(xml.getAbsolutePath(), Long.valueOf(xml.lastModified())); } } else { // Passing null for docBase means that no resources will be // watched. This will be logged at debug level. addWatchedResources(deployedApp, null, context); } // Add the global redeploy resources (which are never deleted) at // the end so they don't interfere with the deletion process addGlobalRedeployResources(deployedApp); } deployed.put(cn.getName(), deployedApp); if (log.isInfoEnabled()) { log.info(sm.getString("hostConfig.deployWar.finished", war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime))); } }至此HostConfig对War解析完成,但是我们也没看到war解压的那一下啊,上面只是对war工程路径,名称,里面包含的Context.xml校验 以及Context节点叫生成,下面请看ContextConfig对象运行流程 他跟HostConfig一样 我们先看lifecycleEvent方法
@Override public void lifecycleEvent(LifecycleEvent event) { // Identify the context we are associated with try { context = (Context) event.getLifecycle(); } catch (ClassCastException e) { log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e); return; } // Process the event that has occurred if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { //===================3========================= configureStart(); } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) { //===================2========================= beforeStart(); } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) { // Restore docBase for management tools if (originalDocBase != null) { context.setDocBase(originalDocBase); } } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) { configureStop(); } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) { //===================1========================= init(); } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) { destroy(); } }我们从上述状态中可以知道 只有三个可能 init(); beforeStart(); configureStart(); 按照顺序找 我们找到before_start才是解析war的关键所在
protected synchronized void beforeStart() { try { //整理server.xml中context下这个的docBase这个文件中的目录 fixDocBase(); } catch (IOException e) { log.error(sm.getString( "contextConfig.fixDocBase", context.getName()), e); } antiLocking(); } /** * Adjust docBase. */ protected void fixDocBase() throws IOException { Host host = (Host) context.getParent(); String appBase = host.getAppBase(); File canonicalAppBase = new File(appBase); //检查server.xml中<host>中AppBase属性的值是不是代表一个绝对路径 if (canonicalAppBase.isAbsolute()) { canonicalAppBase = canonicalAppBase.getCanonicalFile(); } else { canonicalAppBase = new File(getBaseDir(), appBase) .getCanonicalFile(); } //找到server.xml的context中docBase是否存在 String docBase = context.getDocBase(); if (docBase == null) { //docBase不存在 /*如果path不存在或者"/" 或者"/ROOT"这样的形式 * 那么docBase就为ROOT文件加下 * 如果path存在就是path文件加下 */ // Trying to guess the docBase according to the path String path = context.getPath(); if (path == null) { return; } ContextName cn = new ContextName(path, context.getWebappVersion()); docBase = cn.getBaseName(); } File file = new File(docBase); if (!file.isAbsolute()) { docBase = (new File(canonicalAppBase, docBase)).getPath(); } else { docBase = file.getCanonicalPath(); } file = new File(docBase); String origDocBase = docBase; ContextName cn = new ContextName(context.getPath(), context.getWebappVersion()); String pathName = cn.getBaseName(); boolean unpackWARs = true; if (host instanceof StandardHost) { unpackWARs = ((StandardHost) host).isUnpackWARs(); if (unpackWARs && context instanceof StandardContext) { unpackWARs = ((StandardContext) context).getUnpackWAR(); } } /** * 解压war工程 */ if (docBase.toLowerCase(Locale.ENGLISH).endsWith(".war") && !file.isDirectory()) { if (unpackWARs) { URL war = new URL("jar:" + (new File(docBase)).toURI().toURL() + "!/"); //正式对xxx.war进行分析生成xxx工程 docBase = ExpandWar.expand(host, war, pathName); file = new File(docBase); docBase = file.getCanonicalPath(); if (context instanceof StandardContext) { ((StandardContext) context).setOriginalDocBase(origDocBase); } } else { URL war = new URL("jar:" + (new File (docBase)).toURI().toURL() + "!/"); ExpandWar.validate(host, war, pathName); } } else { File docDir = new File(docBase); if (!docDir.exists()) { File warFile = new File(docBase + ".war"); if (warFile.exists()) { URL war = new URL("jar:" + warFile.toURI().toURL() + "!/"); if (unpackWARs) { docBase = ExpandWar.expand(host, war, pathName); file = new File(docBase); docBase = file.getCanonicalPath(); } else { docBase = warFile.getCanonicalPath(); ExpandWar.validate(host, war, pathName); } } if (context instanceof StandardContext) { ((StandardContext) context).setOriginalDocBase(origDocBase); } } } if (docBase.startsWith(canonicalAppBase.getPath() + File.separatorChar)) { docBase = docBase.substring(canonicalAppBase.getPath().length()); docBase = docBase.replace(File.separatorChar, '/'); if (docBase.startsWith("/")) { docBase = docBase.substring(1); } } else { docBase = docBase.replace(File.separatorChar, '/'); } context.setDocBase(docBase); }
//正式对xxx.war进行分析生成xxx工程 docBase = ExpandWar.expand(host, war, pathName); 这段中开始生成真正的工程代码如下
public static String expand(Host host, URL war, String pathname) throws IOException { // Make sure that there is no such directory already existing File appBase = new File(host.getAppBase()); if (!appBase.isAbsolute()) { appBase = new File(System.getProperty(Globals.CATALINA_BASE_PROP), host.getAppBase()); } if (!appBase.exists() || !appBase.isDirectory()) { throw new IOException (sm.getString("hostConfig.appBase", appBase.getAbsolutePath())); } //工程路径存在吗 存在说明已经解压好了 File docBase = new File(appBase, pathname); if (docBase.exists()) { // War file is already installed return (docBase.getAbsolutePath()); } //不存在则创建 工程路径,一般以war文件名创建路径 // Create the new document base directory if(!docBase.mkdir() && !docBase.isDirectory()) throw new IOException(sm.getString("expandWar.createFailed", docBase)); // Expand the WAR into the new document base directory String canonicalDocBasePrefix = docBase.getCanonicalPath(); if (!canonicalDocBasePrefix.endsWith(File.separator)) { canonicalDocBasePrefix += File.separator; } /** * 下面是以jar方式访问war工程中的文件 * 然后将war工程xxx内部的文件全部复制到工程xxx目录下 */ JarURLConnection juc = (JarURLConnection) war.openConnection(); juc.setUseCaches(false); JarFile jarFile = null; InputStream input = null; boolean success = false; try { jarFile = juc.getJarFile(); Enumeration<JarEntry> jarEntries = jarFile.entries(); while (jarEntries.hasMoreElements()) { JarEntry jarEntry = jarEntries.nextElement(); String name = jarEntry.getName(); File expandedFile = new File(docBase, name); if (!expandedFile.getCanonicalPath().startsWith( canonicalDocBasePrefix)) { // Trying to expand outside the docBase // Throw an exception to stop the deployment throw new IllegalArgumentException( sm.getString("expandWar.illegalPath",war, name, expandedFile.getCanonicalPath(), canonicalDocBasePrefix)); } int last = name.lastIndexOf('/'); if (last >= 0) { File parent = new File(docBase, name.substring(0, last)); if (!parent.mkdirs() && !parent.isDirectory()) { throw new IOException( sm.getString("expandWar.createFailed", parent)); } } if (name.endsWith("/")) { continue; } input = jarFile.getInputStream(jarEntry); if(null == input) throw new ZipException(sm.getString("expandWar.missingJarEntry", jarEntry.getName())); // Bugzilla 33636 expand(input, expandedFile); long lastModified = jarEntry.getTime(); if ((lastModified != -1) && (lastModified != 0)) { expandedFile.setLastModified(lastModified); } input.close(); input = null; } success = true; } catch (IOException e) { throw e; } finally { if (!success) { // If something went wrong, delete expanded dir to keep things // clean deleteDir(docBase); } if (input != null) { try { input.close(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } input = null; } if (jarFile != null) { try { jarFile.close(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); } jarFile = null; } } // Return the absolute path to our new document base directory return (docBase.getAbsolutePath()); }上述代码我们可以知道war这样的工程解压是,通过jar形式的api访问war内部文件 然后复制到工程文件夹一种解析 2, 大家有没有注意到 ,平时我们启动了tomat以后 只要修改工程里的文件 马上他就会重新部署这一现象,这是因为tomat 会检测webapps 下工程改动变换,从而调整做出新的部署 代码如下ContainerBase 中threadStart 方法
protected void threadStart() { if (thread != null) return; if (backgroundProcessorDelay <= 0) return; threadDone = false; String threadName = "ContainerBackgroundProcessor[" + toString() + "]"; thread = new Thread(new ContainerBackgroundProcessor(), threadName); thread.setDaemon(true); thread.start(); }ContainerBackgroundProcessor启动容器守护线程 做这一件事,以每隔backgroundProcessorDelay秒的执行
protected class ContainerBackgroundProcessor implements Runnable { @Override public void run() { Throwable t = null; String unexpectedDeathMessage = sm.getString( "containerBase.backgroundProcess.unexpectedThreadDeath", Thread.currentThread().getName()); try { while (!threadDone) { try { Thread.sleep(backgroundProcessorDelay * 1000L); } catch (InterruptedException e) { // Ignore } if (!threadDone) { Container parent = (Container) getMappingObject(); ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (parent.getLoader() != null) { cl = parent.getLoader().getClassLoader(); } processChildren(parent, cl); } } } catch (RuntimeException e) { t = e; throw e; } catch (Error e) { t = e; throw e; } finally { if (!threadDone) { log.error(unexpectedDeathMessage, t); }
protected void processChildren(Container container, ClassLoader cl) { try { if (container.getLoader() != null) { Thread.currentThread().setContextClassLoader (container.getLoader().getClassLoader()); } //这里container是StandardHost container.backgroundProcess(); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.error("Exception invoking periodic operation: ", t); } finally { Thread.currentThread().setContextClassLoader(cl); } Container[] children = container.findChildren(); for (int i = 0; i < children.length; i++) { if (children[i].getBackgroundProcessorDelay() <= 0) { processChildren(children[i], cl); } } } }StandardHost 方法可知
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); }激活状态的 Lifecycle.PERIODIC_EVENT 代码如: fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null); 此状态运行 我们在前面代码HostConfig中可以知道
HostConfig中lifecycleEvent方法如下 public void lifecycleEvent(LifecycleEvent event) { // Identify the host we are associated with try { host = (Host) event.getLifecycle(); if (host instanceof StandardHost) { setCopyXML(((StandardHost) host).isCopyXML()); setDeployXML(((StandardHost) host).isDeployXML()); setUnpackWARs(((StandardHost) host).isUnpackWARs()); setContextClass(((StandardHost) host).getContextClass()); } } catch (ClassCastException e) { log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e); return; } // Process the event that has occurred if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) { check(); } else if (event.getType().equals(Lifecycle.START_EVENT)) { start(); } else if (event.getType().equals(Lifecycle.STOP_EVENT)) { stop(); } }综上可以 tomcat定时的去扫描webapps目录下工程的变化