前情回顾

上一篇文章主要了解了一下Tomcat启动入口,以及初步的分析了Tomcat的启动流程,下面我们将会解密Tomcat应用部署的实际流程。

一、直观对比

虽然前面已经说了那么多关于Tomcat的东西,但是我相信绝大部分同学应该都没有专门的去研究过Tomcat的内部实现。我们接触最多的应该还是上传一个war包丢在webapps目录下,然后重启一下Tomcat服务器(甚至不重启)。下面我们以图形的形式,直观的对比Tomcat各组件的关系。

二、应用部署与加载流程分析

下面就针对应用部署与加载流程展开分析

2.1 部署方式

  • 隐式部署

直接丢文件夹、war、jar 到 webapps 目录,tomcat 会根据文件夹名称自动生成虚拟路径,简单,但是需要重启 Tomcat 服务器,包括要修改端口和访问路径的也需要重启。

  • 显示部署

添加 context 元素 server.xml 中的 Host 加入一个 Context(指定路径和文件地址),例如:

<Host name="localhost"><Context path="/myapp" docBase="/opt/work_tomcat/myapp.war" />
</Host>

即/myapp 这个虚拟路径映射到了 /opt/work_tomcat/myapp 目录下(war 会解压成文件),修改完 server.xml 需要重启 tomcat 服务器。

  • 创建 xml 文件

在 conf/Catalina/localhost 中创建 xml 文件,访问路径为文件名,例如:在 localhost 目录下新建 demo.xml,内容为:

<Context docBase="/opt/work_tomcat/myapp" />

不需要写 path,虚拟目录就是文件名 demo,path 默认为/demo,添加 demo.xml 不需要重启 tomcat 服务器。

2.2 Web应用加载

Web应用加载属于Server启动的核心处理过程。Catalina对Web应用的加载主要由

  • StandardHost、
  • HostConfig、
  • StandardContext、
  • ContextConfig、
  • StandardWrapper

这5个类完成。

2.2.1 StandardHost

  1. 当显示部署时,Context元素将会作为Host容器的子容器添加到Host实例当中,并在Host启动时,由生命周期管理接口的start()方法启动。
  2. 大多数情况下,我们使用的其实都是隐式部署。我们需要关注的是Digester解析器默认为StandardHost容器添加了一个HostConfig监听器。
@Override
publicvoid addRuleInstances(Digester digester) {digester.addObjectCreate(prefix + "Host","org.apache.catalina.core.StandardHost","className");digester.addSetProperties(prefix + "Host");digester.addRule(prefix + "Host",new CopyParentClassLoaderRule());digester.addRule(prefix + "Host",new LifecycleListenerRule("org.apache.catalina.startup.HostConfig","hostConfigClass"));//省略部分代码...
}

2.2.2 HostConfig

HostConfig处理的生命周期事件包括:

  • START_EVENT
  • PERIODIC_EVENT
  • STOP_EVENT

其中,前两者都与Web应用部署密切相关,后者用于在Host停止时注销其对应的MBean。逻辑在Host启动时触发START_EVENT事件,完成服务器启动过程中的Web应用部署(只有当Host的deployOnStartup属性为true时,服务器才会在启动过程中部署Web应用,该属性默认值为true)。

public class HostConfig implements LifecycleListener {/*** Process a "start" event for this Host.*/public void start() {//省略部分代码...if (host.getDeployOnStartup()) {//部署当前虚拟机的应用deployApps();}}protected void deployApps() {//默认为¨E95EBASE/webappsFileappBase¨E61Ehost.getAppBaseFile();//默认为CATALINA_BASE/webappsFile appBase = host.getAppBaseFile();//默认为CATALINA¨E95EBASE/webappsFileappBase¨E61Ehost.getAppBaseFile();//默认为CATALINA_BASE/conf/<Engine名称>/<Host名称>File configBase = host.getConfigBaseFile();String[] filteredAppPaths = filterAppPaths(appBase.list());// Deploy XML descriptors from configBase 描述文件部署deployDescriptors(configBase, configBase.list());// Deploy WARs War包部署deployWARs(appBase, filteredAppPaths);// Deploy expanded folders 目录部署deployDirectories(appBase, filteredAppPaths);}
}
  • Context描述文件部署
  1. 扫描$CATALINA_BASE/conf/<Engine名称>/<Host名称>目录下的xml文件 。
  2. 部署描述文件应用的详见HostConfig.deployDescriptor。
  • War包部署
  1. 过滤$CATALINA_BASE/webapps目录下所有符合条件的WAR包:不符合deployIgnore的过滤规则、文件名不为META-INF和WEB-INF、以war作为扩展名的文件。
  2. 部署WAR包应用的过程详见HostConfig.deployWAR。
  • Web目录部署
  1. 过滤CATALINA_BASE/webapps目录下所有符合条件的WAR包:不符合deployIgnore的过滤规则、文件名不为META-INF和WEB-INF、以war作为扩展名的文件。
  2. 部署Web目录应用的过程详见HostConfig.deployDirectory。

逻辑对于上述自动部署过程中,我们可以发现,经过一系列的条件判断,最终工作就是构建了一个Context实例,并添加ContextConfig生命周期监听器。

通过Host的addChild()方法将Context实例添加到Host。并在Host启动时启动Context。并根据不同的部署方式添加文件到守护资源,以便文件发生变更时重新部署或者加载Web应用。

逻辑在Container容器的backgroundProcess()定期扫描Web应用发生变更,并从新加载处理完成之后触发PERIODIC_EVENT事件。

在HostConfig中通过DeployedApplication维护了两个守护资源列表:redeployeResources和reloadResources,前者用于守护导致应用重新部署的资源,后者守护导致应用重新加载的资源。两个列表分别维护了资源及其最后修改的时间。

当HostConfig接收到PERIODIC_EVENT事件后,会检测守护资源的变更情况。如果发生变更,将重新加载或者部署应用以及更新资源的最后修改时间。

2.2.3 StandardContext

WebappLoader

需要特别关注的是在StandardContext.startInternal()方法中,每个Context都创建了一个WebappLoader应用类加载器。那么它到底具备什么特殊的意义呢?

public class StandardContext extends ContainerBaseimplements Context, NotificationEmitter {protected synchronized void startInternal() throws LifecycleException {//每个context新建一个应用类加载器if (getLoader() == null) {WebappLoader webappLoader = new WebappLoader();webappLoader.setDelegate(getDelegate());setLoader(webappLoader);}}public void setLoader(Loader loader) {if (getState().isAvailable() && (loader != null) &&(loader instanceof Lifecycle)) {try {//执行webapploader.starter ==> webappclassloader.startInternal((Lifecycle) loader).start();} catch (LifecycleException e) {log.error(sm.getString("standardContext.setLoader.start"), e);}}}
}
public abstract class WebappClassLoaderBase extends URLClassLoaderimplements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {public void start() throws LifecycleException {state = LifecycleState.STARTING_PREP;//只加载当前context下的类,应用级别隔离WebResource[] classesResources = resources.getResources("/WEB-INF/classes");for (WebResource classes : classesResources) {if (classes.isDirectory() && classes.canRead()) {localRepositories.add(classes.getURL());}}//只加载当前context下的jar包,应用级别隔离WebResource[] jars = resources.listResources("/WEB-INF/lib");for (WebResource jar : jars) {if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {localRepositories.add(jar.getURL());jarModificationTimes.put(jar.getName(), Long.valueOf(jar.getLastModified()));}}state = LifecycleState.STARTED;}
}

打破了双亲委派模型:

  1. 首先从JVM的Bootstrap类加载器加载;
  2. 优先加载WEB-INF/classes,WEB-INF/lib;
  3. 然后再按照System,Common,Shared的顺序加载。

这么设计的目的主要是考虑到以下三个方面:

  1. 逻辑隔离性:Web应用类库相互隔离,避免依赖库或者应用包相互影响。比如有两个应用分别采用了Spring2.5和Spring5.0,如果应用服务器使用同一个类加载器加载,那么Web应用将会由于Jar包覆盖而导致无法启动成功。
  2. 灵活性:既然Web应用之间的类加载器相互独立,那么我们就能只针对一个Web应用进行重新部署,此事该Web应用的类加载器将会重新创建,而且不会影响其他Web应用。如果共用一个类加载器显然无法实现,因为只有一个类加载器的时候,类质检的依赖是杂乱无章的,无法完整的移出某一个Web应用的类。
  3. 性能:由于每个Web应用都有一个类加载器,因此Web应用再加载类时,不会搜索其他应用包含的Jar包,性能自然高于应用服务器只有一个类加载器的情况。

2.2.4 ContextConfig

当我们在创建Context的时候会同时创建ContextConfig作为它的状态监听器,在Context执行startInternal()方法时,会发布一个Lifecycle.CONFIGURE_START_EVENT事件通知ContextConfig做后续的工作。

需要注意的是:

① 当触发AFTER_INIT_EVENT事件时,解析ConfigFile文件,按优先级顺序从高到底依次为:

  1. Web应用配置(META-INF/context.xml)。
  2. Host配置(conf/context.xml.default)。
  3. Catalina配置(conf/context.xml)。

② 当触发BEFORE_START_EVENT事件时,会执行ExpandWar.expand方法去解压war包。

③ 当触发CONFIGURE_START_EVENT事件时,ContextConfig.webConfig()方法会解析web.xml,创建Servlet,Filter,ServletContextListener等Web容器相关的对象从而完成初始化。

2.2.5 StandardWrapper

StandardWrapper具体维护了Servlet实例,当ContextConfig完成初始化之后,会根据WebXml中的Servlet定义创建Wrapper。创建Servlet实例,执行javax.servlet.Servlet.init()完成Servlet的初始化。

TIP:如果想要详细了解服务启动及加载的流程图可以查看官网提供的资料 http://tomcat.apache.org/tomcat-9.0-doc/architecture/startup/serverStartup.pdf

三、本文小结

我们发现Tomcat可以部署多个应用,每个Context则对应了一个应用,应用部署的方式可以是文件夹也可以是war包,如果是war包部署,它还会自动帮我们将war包解压出来。每个应用中又有各自的Servlet。

至此我们可以说Tomcat将应用已经部署完毕,下次我们将分析一个普通的HTTP请求是如何经过网络层,到达我们的Tomcat,再经过我们的应用处理,最后返回出请求结果。


程序员的核心竞争力其实还是技术,因此对技术还是要不断的学习,关注 “IT 巅峰技术” 公众号 ,该公众号内容定位:中高级开发、架构师、中层管理人员等中高端岗位服务的,除了技术交流外还有很多架构思想和实战案例。

作者是 《 消息中间件 RocketMQ 技术内幕》 一书作者,同时也是 “RocketMQ上海社区”联合创始人,曾就职于拼多多、德邦等公司,现任上市快递公司架构负责人,主要负责开发框架的搭建、中间件相关技术的二次开发和运维管理、混合云及基础服务平台的建设。

Tomcat:应用加载原理分析相关推荐

  1. JPA/hibernate懒加载原理分析及JSON格式API反序列化时连环触发懒加载问题的解决

    什么是懒加载 JPA是java持久层的API,也就是java官方提供的一个ORM框架,Spring data jpa是spring基于hibernate开发的一个JPA框架.Spring data j ...

  2. so链接及动态加载原理分析

    http://www.ibm.com/developerworks/cn/linux/l-dynlink/ 程序的链接和装入及Linux下动态链接的实现 程序的链接和装入存在着多种方法,而如今最为流行 ...

  3. 内嵌WEB服务器加载原理

    内嵌WEB服务器加载原理 理解里面的tomcat是如何启动的 Startup.bat Server.start() 1,概述 我们在使用springboot项目的时候并没有使用外部的tomcat,那么 ...

  4. 浅析Android字体加载原理

    浅析Android字体加载原理 前言 之前在处理系统字体问题的时候,可借鉴的资料很少,遇到了很多坑,不得不了解Android字体加载原理,现抽空写一篇总结,来加深自己对这块的理解. 内容 概述 And ...

  5. 【Docker镜像文件加载原理生产中重新制作并提交镜像文件案例演示】

    一.知识回顾 之前的内容都帮你整理好了,在这里哟! [0.Docker相关目录文章整理,可自行查看,包含多节内容] [1.Docker详细安装部署&阿里镜像地址配置] [2.Docker架构& ...

  6. 【Docker篇】Docker镜像加载原理,UnionFS(联合文件系统),镜像Commit

    文章目录 Docker镜像 1. 镜像是什么 2.Docker镜像加载原理 2.1 UnionFS(联合文件系统) 2.2 Docker镜像加载原理 3. 分层理解 3.1 引申理解 4. 镜像Com ...

  7. 一个驱动无法加载的分析

    一个驱动无法加载的分析 客户反馈一个问题,原工作很好的usb key设备,安装 NCT_2000_XP 后,运行测试程序找硬件,提示没找到.检查系统 %systemroot%/system32/dir ...

  8. spring bean加载原理

    简单的分析了一下spring bean的加载原理,属于个人的理解,源码比这个要复杂的多: spring的配置文件applicationContext.xml的内容如下: <?xml versio ...

  9. 深度剖析React懒加载原理

    目录 代码分割 React的懒加载 import() 原理 React.lazy 原理 Suspense 原理 参考 1.代码分割 (1)为什么要进行代码分割? 现在前端项目基本都采用打包技术,比如 ...

最新文章

  1. python文件读取方法read(size)的含义是_在Python中可使用read([size])来读取文件中的数据,如果参数size省略,则读取文件中的()。...
  2. docker挂载NVIDIA显卡
  3. 《基于张量网络的机器学习入门》学习笔记4
  4. easy and hard things
  5. 动机模型_解读冰山模型:强烈的动机是成功的开始
  6. c语言斐波那契数列_神奇的数列——斐波那契数列
  7. 【Pytorch神经网络实战案例】22 基于Cora数据集实现图注意力神经网络GAT的论文分类
  8. gem5的安装、编译及运行
  9. java爬虫技术的作用_Java网络爬虫怎么实现?
  10. linux 怎么关闭输入法快捷键设置方法,关闭输入法快捷键
  11. TeamFlowy——结合Teambition与Workflowy
  12. Windows7不停弹出计算机界面,拨号连接自动弹出,教您如何解决电脑总是自动弹出...
  13. html怎么将一张图撑大,div 如何防止图片太大被撑开
  14. Android 知识点 108 —— PowerManagerService
  15. 从图(Graph)到图卷积(Graph Convolution):漫谈图神经网络模型 (二)
  16. 电影《风雨哈佛路》经典台词
  17. css 水平居中和垂直居中
  18. 3D游戏建模师的工资和发展前景到底怎么样?
  19. 产品设计思维导图模板
  20. springboot 拦截器 及 可能失效原因

热门文章

  1. 1700. 无法吃午餐的学生数量
  2. 【FPGA】FPGA的介绍及入门
  3. 吃一堑长一智(sql篇)
  4. js 获取本月、本周、本年年初
  5. html或jsp页面跳转
  6. SKU,UPC,ASIN,EAN,GCID到底是什么鬼
  7. 【HOG原理与训练】HOG(方向梯度直方图)
  8. pr / twixtor 补帧
  9. 如何使用 Qt 开发音视频通话应用
  10. 8.3 Dirac定理(1952)