[关闭]
@w1992wishes 2018-03-09T13:46:54.000000Z 字数 20432 阅读 1210

Tomcat源码分析 -- Web应用加载(Context的创建)

源码分析 tomcat


本篇结构:

一、前言

上篇介绍了整个Server的创建,其中涉及Context的创建并没有具体分析,只就server.xml配置文件中配置<Context>标签这一种情况进行了简单分析,这无疑是不完整的,会产生一些疑惑。

默认情况下,server.xml文件中并没有<Context>标签啊,这是怎么回事?

平常都是直接将web应用直接放在tomcat的webapp目录下,启动tomcat就可以访问,这时Web应用是怎么部署的?或者说Context实例是怎么创建的?

本篇就这个问题进行探讨。

二、Tomcat部署Web应用的3种方式

先讨论一下Tomcat下部署Web应用的方式。

2.1、第一种:项目直接放入 webapps 目录中

将编写并且编译好的Web项目(注意是要编译好的),放入到Tomcat的webapps目录下面,可以是一个war包,也可以是一个解压后的web应用。

以我电脑中的应用为例,我将一个dubbo-admin项目通过maven打包,然后放入Tomcat的webapps下,然后启动Tomcat(当然这个项目需要先启动zookeeper),在浏览器中输入http://localhost:8080/dubbo-admin-2.5.4-SNAPSHOT就可以正常访问这个项目:

你可以找一个web应用以同样的方式部署到Tomcat的webapps目录下。

2.2、第二种:修改 conf/server.xml 文件

打开tomcat下conf/server.xml,在<Host> </Host>标签之间输入项目配置信息:

2.3、第三种:增加自定义web部署文件(推荐使用,不需要重启Tomcat)

这种方式和方法2差不多,但不是在Server.xml文件中添加Context标签,而是在$CATALINA_HOME/conf/Catalina/localhost中添加一个xml文件(这里要说明Catalina是Engine名字,localhost是Host名字)。

这种方式部署,文件名字就是访问路径(http://localhost:8080/dubbo-admin),不需要配置path,配置了也不起作用。

2.4、三种方式对比

总结:

  1. 第一种方法比较普通,日常用的比较多,也可以用context描述文件对Context进行定制,但无法覆盖path和docBase两个属性,所以此种部署方式无法自定义Web应用的的部署目录。
  2. 第二种方法直接在server.xml文件中配置,但是从tomcat5.0版本开始后,server.xml文件作为tomcat启动的主要配置文件,一旦tomcat启动后,便不会再读取这个文件,因此无法在tomcat服务启动后发布web项目,灵活性稍差,但可配置性最强。
  3. 第三种方法是最好的,每个项目分开配置,tomcat将以\conf\Catalina\localhost目录下的xml文件的文件名作为web应用的上下文路径,而不再理会中配置的path路径,因此在配置的时候,可以不写path。

三、从源码角度看看Web应用部署

上篇有提到,HostConfig是在创建Host实例时默认添加到Host实例中的生命周期监听器。Tomcat启动后,经过Catalina的start方法一层层调用各组件,并触发相应的生命周期事件,HostConfig就是在这个阶段触发的,事件触发后,就来到lifecycleEvent这个方法。

  1. public void lifecycleEvent(LifecycleEvent event) {
  2. // Identify the host we are associated with
  3. try {
  4. host = (Host) event.getLifecycle();
  5. if (host instanceof StandardHost) {
  6. setCopyXML(((StandardHost) host).isCopyXML());
  7. setDeployXML(((StandardHost) host).isDeployXML());
  8. setUnpackWARs(((StandardHost) host).isUnpackWARs());
  9. setContextClass(((StandardHost) host).getContextClass());
  10. }
  11. } catch (ClassCastException e) {
  12. log.error(sm.getString("hostConfig.cce", event.getLifecycle()), e);
  13. return;
  14. }
  15. // Process the event that has occurred
  16. if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
  17. check();
  18. } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
  19. beforeStart();
  20. } else if (event.getType().equals(Lifecycle.START_EVENT)) {
  21. start();
  22. } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
  23. stop();
  24. }
  25. }

这个方法中,重点看Lifecycle.START_EVENT事件的触发后的动作,又start()方法执行。

  1. public void start() {
  2. if (log.isDebugEnabled())
  3. log.debug(sm.getString("hostConfig.start"));
  4. try {
  5. ObjectName hostON = host.getObjectName();
  6. oname = new ObjectName
  7. (hostON.getDomain() + ":type=Deployer,host=" + host.getName());
  8. Registry.getRegistry(null, null).registerComponent
  9. (this, oname, this.getClass().getName());
  10. } catch (Exception e) {
  11. log.error(sm.getString("hostConfig.jmx.register", oname), e);
  12. }
  13. if (!host.getAppBaseFile().isDirectory()) {
  14. log.error(sm.getString("hostConfig.appBase", host.getName(),
  15. host.getAppBaseFile().getPath()));
  16. host.setDeployOnStartup(false);
  17. host.setAutoDeploy(false);
  18. }
  19. if (host.getDeployOnStartup())
  20. deployApps();
  21. }

在start方法中,可以看到只有当Host的deployOnStartup属性为true时,服务器才会在启动过程中部署Web应用,默认是为true。

来看deployApps()方法。

  1. protected void deployApps() {
  2. File appBase = host.getAppBaseFile();
  3. File configBase = host.getConfigBaseFile();
  4. String[] filteredAppPaths = filterAppPaths(appBase.list());
  5. // Deploy XML descriptors from configBase
  6. deployDescriptors(configBase, configBase.list());
  7. // Deploy WARs
  8. deployWARs(appBase, filteredAppPaths);
  9. // Deploy expanded folders
  10. deployDirectories(appBase, filteredAppPaths);
  11. }

appBase文件是Host标签中的appBase属性的值,即指webapps目录,configBase指向的是$CATALINA_HOME/conf/Catalina/localhost目录。

可以看到具体有三个应用部署的方法:

1.deployDescriptors()方法对应前面说的第三种部署方式,即在$CATALINA_HOME/conf/Catalina/localhost目录下放置xml文件,Context描述文件部署。

2.deployWARs()方法对应前面提到的第一种部署方式,在webapps目录下放置war包。

3.deployDirectories()方法也是对应第一种部署方式,同deployWARs方法相差不多。

至于提到的第二种部署方式,在server.xml中配置Context标签的方式,前面提到过,是在server.xml文件的解析过程中进行的。

下面就这三个方法分别讨论。

四、Context描述文件部署--deployDescriptors

Tomcat支持通过一个独立的Context描述文件来配置并启动Web应用,配置方式同server.xml中的<Context>元素。该文件的放置文件由Host的xmlBase属性指定,如果未指定,则默认为$CATALINA_HOME/conf/<Engine名称>/<Host名称>,可通过HostConfig的deployApps方法中得到验证。

deployDescriptors方法如下:

  1. protected void deployDescriptors(File configBase, String[] files) {
  2. if (files == null)
  3. return;
  4. ExecutorService es = host.getStartStopExecutor();
  5. List<Future<?>> results = new ArrayList<>();
  6. for (int i = 0; i < files.length; i++) {
  7. File contextXml = new File(configBase, files[i]);
  8. if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
  9. ContextName cn = new ContextName(files[i], true);
  10. if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
  11. continue;
  12. results.add(
  13. es.submit(new DeployDescriptor(this, cn, contextXml)));
  14. }
  15. }
  16. for (Future<?> result : results) {
  17. try {
  18. result.get();
  19. } catch (Exception e) {
  20. log.error(sm.getString(
  21. "hostConfig.deployDescriptor.threaded.error"), e);
  22. }
  23. }
  24. }

具体部署过程简单解释如下:

(1)从$CATALINA_HOME/conf/<Engine名称>/<Host名称>目录下获取所有的配置文件,对于这些配置文件,由线程池完成解析部署。
(2)对于每个文件的部署线程,进行的操作都交由deployDescriptor()方法(deployDescriptor()方法比较长,就不列了,有兴趣可以自己下载源码进去看)。deployDescripto()方法主要做如下事情:

①首先使用Digester解析Context描述文件,创建Context实例。

  1. try (FileInputStream fis = new FileInputStream(contextXml)) {
  2. synchronized (digesterLock) {
  3. try {
  4. context = (Context) digester.parse(fis);
  5. } finally {
  6. digester.reset();
  7. if (context == null) {
  8. context = new FailedContext();
  9. }
  10. }
  11. }

②为Context实例添加ContextConfig生命周期监听器。

  1. Class<?> clazz = Class.forName(host.getConfigClass());
  2. LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
  3. context.addLifecycleListener(listener);

③更新Context的名称和路径等,其名字和路径是解析Context描述文件的名字所来,所以说这种情况下在Context中配置path属性无效。(这里说无效是因为在后续的Context的生命周期监听器中ContextConfig的AFTER_INIT_EVENT事件中会再赋值一遍,但path会被过滤,name是可以设置的。)

  1. context.setConfigFile(contextXml.toURI().toURL());
  2. context.setName(cn.getName());
  3. context.setPath(cn.getPath());
  4. context.setWebappVersion(cn.getVersion());

④将Context描述文件、Web应用目录及web.xml添加到守护资源,以便文件发生变更时(判断依据是修改时间),重新部署或者加载Web应用。

  1. if (context.getDocBase() != null) {
  2. File docBase = new File(context.getDocBase());
  3. if (!docBase.isAbsolute()) {
  4. docBase = new File(host.getAppBaseFile(), context.getDocBase());
  5. }
  6. // If external docBase, register .xml as redeploy first
  7. if (!docBase.getCanonicalPath().startsWith(
  8. host.getAppBaseFile().getAbsolutePath() + File.separator)) {
  9. isExternal = true;
  10. deployedApp.redeployResources.put(
  11. contextXml.getAbsolutePath(),
  12. Long.valueOf(contextXml.lastModified()));
  13. deployedApp.redeployResources.put(docBase.getAbsolutePath(),
  14. Long.valueOf(docBase.lastModified()));
  15. if (docBase.getAbsolutePath().toLowerCase(Locale.ENGLISH).endsWith(".war")) {
  16. isExternalWar = true;
  17. }
  18. } else {
  19. log.warn(sm.getString("hostConfig.deployDescriptor.localDocBaseSpecified",
  20. docBase));
  21. // Ignore specified docBase
  22. context.setDocBase(null);
  23. }

⑤通过Host的addChild方法将Context实例添加到Host中,该方法会判断Host是否启动,如果Host已经启动,Context直接启动。

五、WEB目录部署--deployDirectories

以目录的形式发布并部署Web应用是Tomcat中最常见的的部署方式。只需要将包含Web应用的所有资源文件(HTML、JS、CSS、JSP等)、Jar包、描述文件(WEB-INF/web.xml)的目录复制到Host指定的appBase目录即可完成部署。

如果某个应用不想部署,可以通过Host的deployIgnore属性进行忽略。不指定,所有目录均进行部署(deployApps方法中调用filterAppPaths方法完成)。

该种部署方式下,Catalina同样支持通过配置文件来实例化Context(默认位于Web应用META-INF目录下,名字为context.xml)。我们可以在配置文件中对Context进行定制,但无法覆盖path、docBase这2个属性,它们由Web目录的路径及名称确定(所以此种部署方式无法自定义Web应用的的部署目录)。

Catalina部署Web目录的操作在deployDirectories方法中,主要操作是:

  1. protected void deployDirectories(File appBase, String[] files) {
  2. if (files == null)
  3. return;
  4. ExecutorService es = host.getStartStopExecutor();
  5. List<Future<?>> results = new ArrayList<>();
  6. for (int i = 0; i < files.length; i++) {
  7. if (files[i].equalsIgnoreCase("META-INF"))
  8. continue;
  9. if (files[i].equalsIgnoreCase("WEB-INF"))
  10. continue;
  11. File dir = new File(appBase, files[i]);
  12. if (dir.isDirectory()) {
  13. ContextName cn = new ContextName(files[i], false);
  14. if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
  15. continue;
  16. results.add(es.submit(new DeployDirectory(this, cn, dir)));
  17. }
  18. }
  19. for (Future<?> result : results) {
  20. try {
  21. result.get();
  22. } catch (Exception e) {
  23. log.error(sm.getString(
  24. "hostConfig.deployDir.threaded.error"), e);
  25. }
  26. }
  27. }

(1)对于Host的appBase目录(默认是$CATALINA_BASE/webapps)下所有符合条件的目录(不符合deployIgnore的过滤规则、目录名不为META-INF和WEB-INF),由线程池完成部署。
(2)对于每个目录的操作是在deployDirectory()方法中,大致操作有:

①HostConfig被触发时,会通过setCopyXML,setDeployXML设置copyXML,deployXML这两个属性,它们都来自Host,默认Host的copyXML为false,deployXML为true。以此为前提。

  1. File xml = new File(dir, Constants.ApplicationContextXml);
  2. File xmlCopy =
  3. new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");
  4. boolean copyThisXml = isCopyXML();
  5. boolean deployThisXML = isDeployThisXML(dir, cn);
  6. DeployedApplication deployedApp;
  7. boolean copyThisXml = isCopyXML();
  8. boolean deployThisXML = isDeployThisXML(dir, cn);
  9. if (deployThisXML && xml.exists()) {
  10. synchronized (digesterLock) {
  11. try {
  12. context = (Context) digester.parse(xml);
  13. } catch (Exception e) {
  14. log.error(sm.getString(
  15. "hostConfig.deployDescriptor.error",
  16. xml), e);
  17. context = new FailedContext();
  18. } finally {
  19. digester.reset();
  20. if (context == null) {
  21. context = new FailedContext();
  22. }
  23. }
  24. }
  25. if (copyThisXml == false && context instanceof StandardContext) {
  26. // Host is using default value. Context may override it.
  27. copyThisXml = ((StandardContext) context).getCopyXML();
  28. }
  29. if (copyThisXml) {
  30. Files.copy(xml.toPath(), xmlCopy.toPath());
  31. context.setConfigFile(xmlCopy.toURI().toURL());
  32. } else {
  33. context.setConfigFile(xml.toURI().toURL());
  34. }
  35. } else if (!deployThisXML && xml.exists()) {
  36. // Block deployment as META-INF/context.xml may contain security
  37. // configuration necessary for a secure deployment.
  38. log.error(sm.getString("hostConfig.deployDescriptor.blocked",
  39. cn.getPath(), xml, xmlCopy));
  40. context = new FailedContext();
  41. } else {
  42. context = (Context) Class.forName(contextClass).getConstructor().newInstance();
  43. }

如果Host的deployXML属性值为true(即通过Context描述文件部署),并且存在META-INF/context.xml文件,则使用Digester解析context.xml创建context实例。

如果Context的copyXML属性为true,则将描述文件复制到$CATALINA_HOME/conf/<Engine名称>/<Host名称>目录下,文件名与Web应用目录名相同。

如果deployXML属性为false,但是存在META-INF/context.xml文件,则构造FailedContext(Container的空模式,用于表示Context部署失败)。

其他情况下,根据Host的contextClass属性指定的类型创建Context类型,默认是"org.apache.catalina.core.StandardContext"。此时,除name,path,webappVersion,docBase会根据Web应用目录的路径及名称外,Context所有属性均采用默认配置。

  1. context.setName(cn.getName());
  2. context.setPath(cn.getPath());
  3. context.setWebappVersion(cn.getVersion());
  4. context.setDocBase(cn.getBaseName());

②为Context添加生命周期监听器ContextConfig。

  1. Class<?> clazz = Class.forName(host.getConfigClass());
  2. LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
  3. context.addLifecycleListener(listener);

③通过Host的addChild方法将Context实例添加到Host中,该方法会判断Host是否启动,如果Host已经启动,Context直接启动。
④将Context描述文件、Web应用目录及web.xml添加到守护资源,以便文件发生变更时(判断依据是修改时间),重新部署或者加载Web应用。守护文件因deployXML和copyXML稍有不同。

六、WAR包部署--deployWARs

WAR包部署和Web目录部署基本类似,只是由于WAR包是一个压缩文件,增加了部分针对压缩文件的处理。

来看deployWARs方法:

  1. protected void deployWARs(File appBase, String[] files) {
  2. if (files == null)
  3. return;
  4. ExecutorService es = host.getStartStopExecutor();
  5. List<Future<?>> results = new ArrayList<>();
  6. for (int i = 0; i < files.length; i++) {
  7. if (files[i].equalsIgnoreCase("META-INF"))
  8. continue;
  9. if (files[i].equalsIgnoreCase("WEB-INF"))
  10. continue;
  11. File war = new File(appBase, files[i]);
  12. if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".war") &&
  13. war.isFile() && !invalidWars.contains(files[i]) ) {
  14. ContextName cn = new ContextName(files[i], true);
  15. if (isServiced(cn.getName())) {
  16. continue;
  17. }
  18. if (deploymentExists(cn.getName())) {
  19. DeployedApplication app = deployed.get(cn.getName());
  20. boolean unpackWAR = unpackWARs;
  21. if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
  22. unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
  23. }
  24. if (!unpackWAR && app != null) {
  25. // Need to check for a directory that should not be
  26. // there
  27. File dir = new File(appBase, cn.getBaseName());
  28. if (dir.exists()) {
  29. if (!app.loggedDirWarning) {
  30. log.warn(sm.getString(
  31. "hostConfig.deployWar.hiddenDir",
  32. dir.getAbsoluteFile(),
  33. war.getAbsoluteFile()));
  34. app.loggedDirWarning = true;
  35. }
  36. } else {
  37. app.loggedDirWarning = false;
  38. }
  39. }
  40. continue;
  41. }
  42. // Check for WARs with /../ /./ or similar sequences in the name
  43. if (!validateContextPath(appBase, cn.getBaseName())) {
  44. log.error(sm.getString(
  45. "hostConfig.illegalWarName", files[i]));
  46. invalidWars.add(files[i]);
  47. continue;
  48. }
  49. results.add(es.submit(new DeployWar(this, cn, war)));
  50. }
  51. }

简单介绍部署过程:

(1)对于Host的appBase目录(默认是$CATALINA_BASE/webapps)下所有符合条件的WAR包(不符合deployIgnore的过滤规则、目录名不为META-INF和WEB-INF、以war作为扩展名的文件),由线程池完成部署。
(2)每个WAR包进行的操作在deployWAR()方法进行(如果之前存在已经解压过的Web目录,则不会进行部署)。

deployWAR()方法简单解析:

①如果Host的deployXML属性为true,且在WAR包同名目录下(就是去除了.war的目录下)存在META-INF/context.xml文件,同时Context的copyXML属性为false,则使用该描述文件创建Context实例(用于WAR包解压目录位于部署目录的情况)。

  1. if (deployThisXML && useXml && !copyXML) {
  2. synchronized (digesterLock) {
  3. try {
  4. context = (Context) digester.parse(xml);
  5. } catch (Exception e) {
  6. log.error(sm.getString(
  7. "hostConfig.deployDescriptor.error",
  8. war.getAbsolutePath()), e);
  9. } finally {
  10. digester.reset();
  11. if (context == null) {
  12. context = new FailedContext();
  13. }
  14. }
  15. }

如果Host的deployXML属性为true且在WAR包压缩文件中存在META-INF/context.xml文件,就用该描述文件创建Context实例。

  1. else if (deployThisXML && xmlInWar) {
  2. synchronized (digesterLock) {
  3. try (JarFile jar = new JarFile(war)) {
  4. JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
  5. try (InputStream istream = jar.getInputStream(entry)) {
  6. context = (Context) digester.parse(istream);
  7. }
  8. } catch (Exception e) {
  9. log.error(sm.getString(
  10. "hostConfig.deployDescriptor.error",
  11. war.getAbsolutePath()), e);
  12. } finally {
  13. digester.reset();
  14. if (context == null) {
  15. context = new FailedContext();
  16. }
  17. context.setConfigFile(
  18. UriUtil.buildJarUrl(war, Constants.ApplicationContextXml));
  19. }
  20. }

如果Host的deployXML属性为false,且在WAR包下存在META-INF/context.xml文件,则记录错误日志,并在后续构建部署失败FailedContext对象。

  1. else if (!deployThisXML && xmlInWar) {
  2. // Block deployment as META-INF/context.xml may contain security
  3. // configuration necessary for a secure deployment.
  4. log.error(sm.getString("hostConfig.deployDescriptor.blocked",
  5. cn.getPath(), Constants.ApplicationContextXml,
  6. new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml")));
  7. } else {
  8. context = (Context) Class.forName(contextClass).getConstructor().newInstance();
  9. }

其他情况下,根据Host的contextClass属性指定的类型创建Context类型,默认是"org.apache.catalina.core.StandardContext"。此时,除name,path,webappVersion,docBase会根据Web应用目录的路径及名称外,Context所有属性均采用默认配置。
②如果deployXML为true,且在WAR包中有META-INF/context.xml文件,同时Context的copyXML属性为true,则将context.xml文件复制到$CATALINA-BASE/conf/<Engine名称>/<Host名称>目录下,文件名同WAR包名称。

  1. if (deployThisXML) {
  2. if (host instanceof StandardHost) {
  3. copyThisXml = ((StandardHost) host).isCopyXML();
  4. }
  5. // If Host is using default value Context can override it.
  6. if (!copyThisXml && context instanceof StandardContext) {
  7. copyThisXml = ((StandardContext) context).getCopyXML();
  8. }
  9. if (xmlInWar && copyThisXml) {
  10. // Change location of XML file to config base
  11. xml = new File(host.getConfigBaseFile(),
  12. cn.getBaseName() + ".xml");
  13. try (JarFile jar = new JarFile(war)) {
  14. JarEntry entry = jar.getJarEntry(Constants.ApplicationContextXml);
  15. try (InputStream istream = jar.getInputStream(entry);
  16. FileOutputStream fos = new FileOutputStream(xml);
  17. BufferedOutputStream ostream = new BufferedOutputStream(fos, 1024)) {
  18. byte buffer[] = new byte[1024];
  19. while (true) {
  20. int n = istream.read(buffer);
  21. if (n < 0) {
  22. break;
  23. }
  24. ostream.write(buffer, 0, n);
  25. }
  26. ostream.flush();
  27. }
  28. } catch (IOException e) {
  29. /* Ignore */
  30. }
  31. }
  32. }

③为Context实例添加ContextConfig生命周期监听器。

  1. Class<?> clazz = Class.forName(host.getConfigClass());
  2. LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
  3. context.addLifecycleListener(listener);

④更新Context的名称和路径等,其名字和路径是解析WAR包的名字所来,name、path、webappVersion、docBase这4个属性无法覆盖。

  1. context.setName(cn.getName());
  2. context.setPath(cn.getPath());
  3. context.setWebappVersion(cn.getVersion());
  4. context.setDocBase(cn.getBaseName() + ".war");

⑤通过Host的addChild方法将Context实例添加到Host中,该方法会判断Host是否启动,如果Host已经启动,Context直接启动。
⑥将Context描述文件、WAR包及web.xml添加到守护资源,以便文件发生变更时(判断依据是修改时间),重新部署或者加载Web应用。

七、Web应用的重新加载和重新部署

Catalina容器支持定期执行自身及其子容器的后台处理过程(是由ContainerBase类中的ContainerBackgroundProcessor线程执行的),具体的处理过程在各容器的backgroundProcess()方法中定义。该机制常用于定时扫描Web应用的变更,并进行重新加载。后台任务处理完成,将触发PERIODIC_EVENT事件。

在ContainerBase中有如下代码片段,backgroundProcessorDelay大于0,才会创建ContainerBackgroundProcessor,该值默认来自StandardEngine,默认是10,即创建ContainerBackgroundProcessor线程后,执行一次后台处理,睡眠10s。backgroundProcessorDelay值可以配置。

在上述基础上,来了解Web应用的重新加载和重新部署。

当HostConfig监听到Lifecycle.PERIODIC_EVENT事件后,会执行check()方法。

  1. protected void check() {
  2. if (host.getAutoDeploy()) {
  3. // Check for resources modification to trigger redeployment
  4. DeployedApplication[] apps =
  5. deployed.values().toArray(new DeployedApplication[0]);
  6. for (int i = 0; i < apps.length; i++) {
  7. if (!isServiced(apps[i].name))
  8. checkResources(apps[i], false);
  9. }
  10. // Check for old versions of applications that can now be undeployed
  11. if (host.getUndeployOldVersions()) {
  12. checkUndeploy();
  13. }
  14. // Hotdeploy applications
  15. deployApps();
  16. }
  17. }

该方法会遍历部署的Web应用,调用checkResources()方法,该方法会检查两类守护资源的最后更新时间,一类是redeployResources,一类是reloadResources。这两类资源是在Web应用部署的时候添加到每个DeployedApplication中的。

7.1、重新部署

redeployResources维护的是Context文件描述符、Web应用目录、WAR包等,这些文件有些(不是所有)发生变化,需要重新部署Web应用。可以从代码得到验证。

  1. String[] resources =
  2. app.redeployResources.keySet().toArray(new String[0]);
  3. // Offset the current time by the resolution of File.lastModified()
  4. long currentTimeWithResolutionOffset =
  5. System.currentTimeMillis() - FILE_MODIFICATION_RESOLUTION_MS;
  6. for (int i = 0; i < resources.length; i++) {
  7. File resource = new File(resources[i]);
  8. long lastModified =
  9. app.redeployResources.get(resources[i]).longValue();
  10. if (resource.exists() || lastModified == 0) {
  11. if (resource.lastModified() != lastModified && (!host.getAutoDeploy() ||
  12. resource.lastModified() < currentTimeWithResolutionOffset ||
  13. skipFileModificationResolutionCheck)) {
  14. //第一种情况,守护资源是Web应用目录,直接更新时间
  15. if (resource.isDirectory()) {
  16. // No action required for modified directory
  17. app.redeployResources.put(resources[i],
  18. Long.valueOf(resource.lastModified()));
  19. //第二种,Web应用存在Context描述文件(需要Context的copyXML为true)并且当前变更的是WAR包时
  20. } else if (app.hasDescriptor &&
  21. resource.getName().toLowerCase(
  22. Locale.ENGLISH).endsWith(".war")) {
  23. Context context = (Context) host.findChild(app.name);
  24. String docBase = context.getDocBase();
  25. //如果docBase不以war结尾,则先删除解压目录,然后再重新reload,在context启动前会解压war包
  26. if (!docBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) {
  27. // This is an expanded directory
  28. File docBaseFile = new File(docBase);
  29. if (!docBaseFile.isAbsolute()) {
  30. docBaseFile = new File(host.getAppBaseFile(),
  31. docBase);
  32. }
  33. reload(app, docBaseFile, resource.getAbsolutePath());
  34. } else {
  35. //直接reload
  36. reload(app, null, null);
  37. }
  38. // Update times
  39. app.redeployResources.put(resources[i],
  40. Long.valueOf(resource.lastModified()));
  41. app.timestamp = System.currentTimeMillis();
  42. boolean unpackWAR = unpackWARs;
  43. if (unpackWAR && context instanceof StandardContext) {
  44. unpackWAR = ((StandardContext) context).getUnpackWAR();
  45. }
  46. if (unpackWAR) {
  47. addWatchedResources(app, context.getDocBase(), context);
  48. } else {
  49. addWatchedResources(app, null, context);
  50. }
  51. return;
  52. //第三种,其他情况,先卸载,再重新部署
  53. } else {
  54. // Everything else triggers a redeploy
  55. // (just need to undeploy here, deploy will follow)
  56. undeploy(app);
  57. deleteRedeployResources(app, resources, i, false);
  58. return;
  59. }
  60. }
  61. } else {
  62. // There is a chance the the resource was only missing
  63. // temporarily eg renamed during a text editor save
  64. try {
  65. Thread.sleep(500);
  66. } catch (InterruptedException e1) {
  67. // Ignore
  68. }
  69. // Recheck the resource to see if it was really deleted
  70. if (resource.exists()) {
  71. continue;
  72. }
  73. // Undeploy application
  74. undeploy(app);
  75. deleteRedeployResources(app, resources, i, true);
  76. return;
  77. }
  78. }

根据代码可得出结论(前提是Host的autoDeploy属性是true,即在Host启动后自动部署应用,默认是true):

undeploy方法如下:

  1. private void undeploy(DeployedApplication app) {
  2. if (log.isInfoEnabled())
  3. log.info(sm.getString("hostConfig.undeploy", app.name));
  4. Container context = host.findChild(app.name);
  5. try {
  6. host.removeChild(context);
  7. } catch (Throwable t) {
  8. ExceptionUtils.handleThrowable(t);
  9. log.warn(sm.getString
  10. ("hostConfig.context.remove", app.name), t);
  11. }
  12. deployed.remove(app.name);
  13. }

7.1、重新加载

reloadResources维护的是web.xml文件,当该文件发生变化时,不需要重新部署应用,只需重新加载应用(reload,即先stop,然后start)。

  1. resources = app.reloadResources.keySet().toArray(new String[0]);
  2. boolean update = false;
  3. for (int i = 0; i < resources.length; i++) {
  4. File resource = new File(resources[i]);
  5. if (log.isDebugEnabled()) {
  6. log.debug("Checking context[" + app.name + "] reload resource " + resource);
  7. }
  8. long lastModified = app.reloadResources.get(resources[i]).longValue();
  9. // File.lastModified() has a resolution of 1s (1000ms). The last
  10. // modified time has to be more than 1000ms ago to ensure that
  11. // modifications that take place in the same second are not
  12. // missed. See Bug 57765.
  13. if ((resource.lastModified() != lastModified &&
  14. (!host.getAutoDeploy() ||
  15. resource.lastModified() < currentTimeWithResolutionOffset ||
  16. skipFileModificationResolutionCheck)) ||
  17. update) {
  18. if (!update) {
  19. // Reload application
  20. reload(app, null, null);
  21. update = true;
  22. }
  23. // Update times. More than one file may have been updated. We
  24. // don't want to trigger a series of reloads.
  25. app.reloadResources.put(resources[i],
  26. Long.valueOf(resource.lastModified()));
  27. }
  28. app.timestamp = System.currentTimeMillis();
  29. }

八、总结

关于Tomcat Web应用部署就了解到这了,肯定有不全面甚至没讲清楚的地方,毕竟水平有限。

另外,这也只是我学习的一个记录,放在网上供个参考,感兴趣的自己研究去吧。

最后附一篇关于server.xml配置解析,这时看应该清楚多了。

Tomcat server.xml配置示例

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注