写点什么

Tomcat:应用加载原理分析

作者:IT巅峰技术
  • 2022 年 4 月 09 日
  • 本文字数:4709 字

    阅读完需:约 15 分钟

前情回顾

上一篇文章主要了解了一下 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 监听器。



@Overridepublicvoid 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/webapps File 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 ContainerBase        implements 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 URLClassLoader        implements 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. 逻辑隔离性:

  2. Web 应用类库相互隔离,避免依赖库或者应用包相互影响。比如有两个应用分别采用了 Spring2.5 和 Spring5.0,如果应用服务器使用同一个类加载器加载,那么 Web 应用将会由于 Jar 包覆盖而导致无法启动成功。

  3. 灵活性:

  4. 既然 Web 应用之间的类加载器相互独立,那么我们就能只针对一个 Web 应用进行重新部署,此事该 Web 应用的类加载器将会重新创建,而且不会影响其他 Web 应用。如果共用一个类加载器显然无法实现,因为只有一个类加载器的时候,类质检的依赖是杂乱无章的,无法完整的移出某一个 Web 应用的类。

  5. 性能:

  6. 由于每个 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 上海社区”联合创始人,曾就职于拼多多、德邦等公司,现任上市快递公司架构负责人,主要负责开发框架的搭建、中间件相关技术的二次开发和运维管理、混合云及基础服务平台的建设。

发布于: 刚刚阅读数: 2
用户头像

一线架构师、二线开发、三线管理 2021.12.07 加入

Redis6.X、ES7.X、Kafka3.X、RocketMQ5.0、Flink1.X、ClickHouse20.X、SpringCloud、Netty5等热门技术分享;架构设计方法论与实践;作者热销新书《RocketMQ技术内幕》;

评论

发布
暂无评论
Tomcat:应用加载原理分析_Tomccat_IT巅峰技术_InfoQ写作平台