tomcat + spring mvc原理(六):tomcat WAR包的部署与加载

tomcat + spring mvc原理(六):tomcat WAR包的部署与加载

前言

    单独部署的tomcat服务器在运行中,当开发人员或者运维人员将开发工程的war包部署到服务目录时,服务器会自动进行war包的解包和类的加载运行,整个spring mvc项目就能在服务器上工作了。本文作为tomcat+spring mvc原理系列tomcat部分的原理的最后一讲,就主要分析一下tomcat的这个功能是如何实现的。需要注意的是,这个功能在spring boot的嵌入式tomcat(tomcat-embed)中并没有起作用,因为tomcat-embed只需要针对spring boot的一个spring mvc项目进行处理,是jar包加载运行,并不涉及到多个war包的热部署。

监控的启动原理

    在原理(二)中我们讲了针对容器的状态名和状态事件监听,但是我们没有讲具体的应用。这里补充一下,这个war包的解压缩和部署就用到了原理(二)提到的容器状态监听的原理。
    整个功能的实现是在HostConfig类中,HostConfig类实现了LifecycleListener。顾名思义,我们也能猜测出HostConfig是Host容器的状态监听器。其实,Engine也有响应的状态监听器,但是里面并没有包括什么作用比较大的代码,只是日志相关的打印。为什么会选中Host这个层次做WAR解压和部署相关的工作?答案在前面几篇的文章中有相应的表述。主要是因为

Context和Wrapper是动态添加的,我们在tomcat的指定目录下每添加一个war包,tomcat加载war包时,就可以添加Context和Servlet。原理(一)

状态监听

public class HostConfig implements LifecycleListener {
    ......
    @Override
    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.BEFORE_START_EVENT)) {
            beforeStart();
        } else if (event.getType().equals(Lifecycle.START_EVENT)) {
            start();
        } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
            stop();
        }
    }
    ......
}

    根据状态监听器的基本运作原理(原理(二)),HostConfig是需要实现lifecycleEvent方法的。每个容器状态变化的时候,就会调用相应的监听器中的lifecycleEvent方法来相应相应的变化。可以看到HostConfig主要处理了Host的四种状态,PERIODIC_EVENT、BEFORE_START_EVENT、START_EVENT、STOP_EVENT。

  • check()方法响应PERIODIC_EVENT周期性检查事件,主要是检查资源是否被更改,如果被更改就会重新解压、重新加载。
  • beforeStart()响应BEFORE_START_EVENT状态,主要是设置资源目录等相关配置,为START_EVENT做准备。
  • start()响应START_EVENT状态,这里就是启动是读取配置、解压包、重新加载资源的操作。下面也主要以这个方法为例。
  • stop()大家都也都懂的= =。

部署项目

    在start()主要处理了注册和类本身状态的设置,最后调用了HostConfig的deployApps方法。

protected void deployApps() {

    File appBase = host.getAppBaseFile();
    File configBase = host.getConfigBaseFile();
    String[] filteredAppPaths = filterAppPaths(appBase.list());
    // Deploy XML descriptors from configBase
    deployDescriptors(configBase, configBase.list());
    // Deploy WARs
    deployWARs(appBase, filteredAppPaths);
    // Deploy expanded folders
    deployDirectories(appBase, filteredAppPaths);

}

    首先是deployDescriptors()方法的作用,上面也有注释,是用来获取xml文件中配置。这个方法主要讲各个配置文件目录中的xml文件的内容读取出来,使配置的内容最终设置到容器中(比如Context的配置),这样配置内容就生效了。deployDirectories()部署扩展的文件夹中的内容,比如一些扩展的webapp文件,其中xml配置文件也需要加载、web应用也要启动,和deployWARS方法的内容基本一致,这里不做具体介绍。
    deployWARs()这个方法的名字就很明显了。这个方法的第一步是筛选出目录中有效的war文件,然后进行部署。

protected void deployWARs(File appBase, String[] files) {

        if (files == null)
            return;

        ExecutorService es = host.getStartStopExecutor();
        List<Future<?>> results = new ArrayList<>();

        for (int i = 0; i < files.length; i++) {

            if (files[i].equalsIgnoreCase("META-INF"))
                continue;
            if (files[i].equalsIgnoreCase("WEB-INF"))
                continue;
            File war = new File(appBase, files[i]);
            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;
                }
                //省略内容:筛选掉不合格的直接continue
                ......
                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包可能不止一个,所以用了一个for循环。这里for循环中使用了多线程加载war包,最后是在循环外使用result.get()进行阻塞,保证所有war包加载完之后才能继续。
    循环里面跳过了META-INF目录和WEB-INF目录,因为这两个目录都是配置文件和其他资源文件目录,还有很多这种文件筛选的逻辑我没贴出来。
    最后是用DeployWar类实现多线程,其实这个类也是一个工具人,里面调用的逻辑只是使用HostConfig类的deployWAR()方法部署单个war包。

@Override
public void run() {
    //config为Hostconfig
    config.deployWAR(cn, war);
}

    deployWar里面做的事情就非常多了,代码也比较长。主要实现了

  1. war包解析,解压缩
  2. 配置文件读取
  3. Context类加载,类配置。可以是标准StandardContext,也可以是war中自己定义的。
  4. war中自定义的状态监听器的加载。
  5. Host使用addChild将context实例加入到tomcat的Host配置中。
  6. 对解压缩的war包进行监控,发生变化就会触发上面说到的check()。

当然里面还有其他内容,就不一一列举了,感兴趣的同学可以自行阅读。当Host调用addChild之后,调用的是一切又回到了我们熟悉的原理(二)中介绍的内容:

需要特别注意的是,Container启动子容器的时候不一定是通过init()或者start()中调用相应子容器的生命周期函数。在容器的addChild方法中,也会调用子容器的start()方法,初始化加载和启动子容器,比如host的addChild(context)方法会调用context的start方法。

这个war包配置的Context、Wrapper容器的start()等生命周期方法会被调用,最后这个spring mvc项目部署完成,就可以正常工作了。
    比较有意思的是,代码里显示war包本身也是一种jar包格式,使用了JarFile类进行解析。这就是代码里的挂羊头卖狗肉吧。

本系列文章:
tomcat + spring mvc原理(一):tomcat原理综述和静态架构
tomcat + spring mvc原理(二):tomcat容器初始化加载和启动
tomcat + spring mvc原理(三):tomcat网络请求的监控与处理1
tomcat + spring mvc原理(四):tomcat网络请求的监控与处理2
tomcat + spring mvc原理(五):tomcat Filter组件实现原理

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值