Tomcat源码解析——热部署和热加载原理

热部署

在Tomcat中可以通过Host标签设置热部署,当 autoDeploy为true时,在运行中的Tomcat中丢入一个war包,那么Tomcat不需要重启就可以自动加载该war包。

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true" >

Tomcat的容器中都包含有 backgroundProcessorDelay 属性和 backgroundProcess方法,默认的实现是,在每个容器启动时,当backgroundProcessorDelay大于1时(单位是秒),则会周期性的执行当前容器及所有子容器的backgroundProcess方法。

ContainerBase:
    protected synchronized void startInternal() throws LifecycleException {
        //...省略
        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:
        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();
                        }
                        //note 执行所有子容器的backgroundProcessorDelay方法
                        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());
                }
                //执行当前容器的backgroundProcess
                container.backgroundProcess();
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
            } finally {
                Thread.currentThread().setContextClassLoader(cl);
            }
            //在执行子容器的backgroundProcess
            Container[] children = container.findChildren();
            for (int i = 0; i < children.length; i++) {
                if (children[i].getBackgroundProcessorDelay() <= 0) {
                    processChildren(children[i], cl);
                }
            }
        }

backgroundProcessorDelay的设置在Engine标签中,因为默认实现包含所有子容器,所有在最顶层的容器上配置一个即可。

<Engine name="Catalina" defaultHost="localhost" backgroundProcessorDelay="20">

我们知道war包的管理应该是属于它的上一层容器Host,在Host中并没有重写backgroundProcess方法。

StandardHost:
    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);
    }

Host父类的实现也没有包含自动部署相关的内容,那么只能是在最后通过事件的机制去实现了,最后会发送一个Host的Lifecycle.PERIODIC_EVENT,Host对应的监听器是HostConfig。

HostConfig:
        public void lifecycleEvent(LifecycleEvent event) {
        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;
        }

        //当Host的周期时间到达时,即Host中的backgroundProcess方法被调用时触发
        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();
        }
    }

在HostConfig中,存在Host周期性事件的监听处理。

HostConfig:
    protected void check() {
        //Host的属性,必须设置server.xml中的Host标签autoDeploy属性为true才进行热部署检测
        if (host.getAutoDeploy()) {
            //检测已经部署的是否存在修改,存在删除应用文件夹和卸载Context(通过判断文件的修改时间与上次部署时是否一致),删除之后,下面会当做新增的重新部署
            DeployedApplication[] apps =
                deployed.values().toArray(new DeployedApplication[0]);
            for (int i = 0; i < apps.length; i++) {
                if (!isServiced(apps[i].name))
                    checkResources(apps[i], false);
            }
            if (host.getUndeployOldVersions()) {
                checkUndeploy();
            }
            //主要是部署新增的应用
            deployApps();
        }
    }

    protected synchronized void checkResources(DeployedApplication app,
            boolean skipFileModificationResolutionCheck) {
        String[] resources =
            app.redeployResources.keySet().toArray(new String[0]);
        long currentTimeWithResolutionOffset =
                System.currentTimeMillis() - FILE_MODIFICATION_RESOLUTION_MS;
        for (int i = 0; i < resources.length; i++) {
            File resource = new File(resources[i]);
            if (log.isDebugEnabled())
                log.debug("Checking context[" + app.name +
                        "] redeploy resource " + resource);
            long lastModified =
                    app.redeployResources.get(resources[i]).longValue();
            if (resource.exists() || lastModified == 0) {
                //判断修改时间是否一致
                if (resource.lastModified() != lastModified && (!host.getAutoDeploy() ||
                        resource.lastModified() < currentTimeWithResolutionOffset ||
                        skipFileModificationResolutionCheck)) {
                    //...省略
                    //从已加载的容器中,卸载Context应用
                    undeploy(app);
                    //如果是解压为文件夹的,则删除
                    deleteRedeployResources(app, resources, i, false);
                    return;
            } 
            //... 省略
        }
    }


    protected void deployApps() {
        File appBase = appBase();
        File configBase = configBase();
        //获取Host中appBase下所有的文件
        String[] filteredAppPaths = filterAppPaths(appBase.list());
        //部署xml
        deployDescriptors(configBase, configBase.list());
        //部署War包
        deployWARs(appBase, filteredAppPaths);
        //部署文件夹,也就是静态资源
        deployDirectories(appBase, filteredAppPaths);

    }

    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++) {
            //只处理war包,别的不管
            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;
                }
                //是否已经部署过了
                if (deploymentExists(cn.getName())) {
                    DeployedApplication app = deployed.get(cn.getName());
                    boolean unpackWAR = unpackWARs;
                    if (unpackWAR && host.findChild(cn.getName()) instanceof StandardContext) {
                        unpackWAR = ((StandardContext) host.findChild(cn.getName())).getUnpackWAR();
                    }
                    if (!unpackWAR && 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;
                }
                //检测路径是否正确
                if (!validateContextPath(appBase, cn.getBaseName())) {
                    log.error(sm.getString(
                            "hostConfig.illegalWarName", files[i]));
                    invalidWars.add(files[i]);
                    continue;
                }
                //没有部署过,则提交到线程池中异步处理
                results.add(es.submit(new DeployWar(this, cn, war)));
            }
        }
        //等待所有war包部署完成
        for (Future<?> result : results) {
            try {
                result.get();
            } catch (Exception e) {
                log.error(sm.getString(
                        "hostConfig.deployWar.threaded.error"), e);
            }
        }
    }

在HostConfig中,会周期性的检测部署的文件(主要是war包),是否发生了变化(通过与上次部署时的文件修改时间对比),发生变化则卸载应用并删除部署的应用文件夹;然后继续判断该war包是否已部署(对于修改的已经被删除,此时不算已部署),如果未部署,则通过线程池异步提交部署。

DeployWar:
        public void run() {
            config.deployWAR(cn, war);
        }

HostConfig:
    protected void deployWAR(ContextName cn, File war) {
        //...省略了对war包的解析和获取conext.xml
        Context context = null;
        //创建出该war包的Conext容器
        context = (Context) Class.forName(contextClass).newInstance();
        //添加到子节点,然后启动容器
        host.addChild(context);
        //...省略
        //添加到已部署应用目录中
        deployed.put(cn.getName(), deployedApp);
    }


StandardHost:
    public void addChild(Container child) {
        child.addLifecycleListener(new MemoryLeakTrackingListener());
        if (!(child instanceof Context))
            throw new IllegalArgumentException
                (sm.getString("standardHost.notContext"));
        super.addChild(child);
    }

    private void addChildInternal(Container child) {
        //...省略
        try {
            if ((getState().isAvailable() ||
                    LifecycleState.STARTING_PREP.equals(getState())) &&
                    startChildren) {
                //启动Conext容器
                child.start();
            }
        } catch (LifecycleException e) {
            log.error("ContainerBase.addChild: start: ", e);
            throw new IllegalStateException("ContainerBase.addChild: start: " + e);
        } finally {
            fireContainerEvent(ADD_CHILD_EVENT, child);
        }
    }

在部署war中,异步创建了Conext应用容器并启动该容器,此时还为解压war中的内容,则最终对war包的处理应该是Context的应用容器中。

由于内容太长,此处省略Context启动的部分内容,在Context启动之前,会发送事件Lifecycle.BEFORE_START_EVENT至监听器ContextConfig中。

ContextConfig:
    public void lifecycleEvent(LifecycleEvent event) {

        try {
            context = (Context) event.getLifecycle();
        } catch (ClassCastException e) {
            return;
        }
        if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
            configureStart();
        } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
            //启动之前处理
            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)) {
            init();
        } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
            destroy();
        }
    }

    protected synchronized void beforeStart() {
        try {
            fixDocBase();
        } catch (IOException e) {
        }
        antiLocking();
    }

    protected void fixDocBase()
        throws IOException {
        Host host = (Host) context.getParent();
        //拿到上层目录
        String appBase = host.getAppBase();
        File canonicalAppBase = new File(appBase);
        if (canonicalAppBase.isAbsolute()) {
            canonicalAppBase = canonicalAppBase.getCanonicalFile();
        } else {
            canonicalAppBase =
                new File(getBaseDir(), appBase)
                .getCanonicalFile();
        }
        //war的文件夹名(即war包取掉后缀的名字)
        String docBase = context.getDocBase();
        if (docBase == null) {
            // 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();
        //是否解压war包
        boolean unpackWARs = true;
        if (host instanceof StandardHost) {
            unpackWARs = ((StandardHost) host).isUnpackWARs();
            if (unpackWARs && context instanceof StandardContext) {
                unpackWARs =  ((StandardContext) context).getUnpackWAR();
            }
        }

        if (docBase.toLowerCase(Locale.ENGLISH).endsWith(".war") && !file.isDirectory()) {
            if (unpackWARs) {
                //加载war包
                URL war = new URL("jar:" + (new File(docBase)).toURI().toURL() + "!/");
                //解压war包到应用文件夹中
                docBase = ExpandWar.expand(host, war, pathName);
                file = new File(docBase);
                docBase = file.getCanonicalPath();
                if (context instanceof StandardContext) {
                    ((StandardContext) context).setOriginalDocBase(origDocBase);
                }
            } else {
                //不解压则校验war包即可(URL类加载器可以加载URL的war包)
                URL war =
                        new URL("jar:" + (new File (docBase)).toURI().toURL() + "!/");
                ExpandWar.validate(host, war, pathName);
            }
        }
        }
        //...省略

    }

在ContextConfig中,监听了Context的启动,如果设置了解压war包,则在此处解压war包,完成最终的步骤。

热部署总结:

即Tomcat中存在一个定时任务,该定时任务会去扫描Host下的war包文件,然后文件 修改时间来判断是否存在修改,存在修改则删除卸载Context应用容器;并且当存在新增 时,则会异步的创建该Context应用容器,如果设置了解压最后还会解压成文件夹,从而完 成了热部署机制的实现。

热加载

在Tomcat中,可以通过Context标签设置热加载,reloadable设置为true时生效,一般调试时才使用,对性能消耗很大。

<Context docBase="app-test" reloadable="true" path=""/>

热加载是属于Conext应用容器的,也是在定时任务的backgroundProcess方法中实现,毕竟线程是属于重量级资源,能在一个线程实现就少创建一个线程。

StandardContext:
        public void backgroundProcess() {
        
        //...省略
        if (loader != null) {
            try {
                //热加载war包下的所有资源(WEB-INF/classes、WEB-INF/lib)
                loader.backgroundProcess();
            } catch (Exception e) {             
            }
        }
        //...省略
    }

WebappLoader:
    public void backgroundProcess() {
        //设置了server.xml中的Context中的reloadable为true时并且发生变化时才会重新加载
        if (reloadable && modified()) {
            try {
                Thread.currentThread().setContextClassLoader
                    (WebappLoader.class.getClassLoader());
                if (container instanceof StandardContext) {
                    //重新加载容器
                    ((StandardContext) container).reload();
                }
            } finally {
                if (container.getLoader() != null) {
                    Thread.currentThread().setContextClassLoader
                        (container.getLoader().getClassLoader());
                }
            }
        } else {
            closeJARs(false);
        }
    }
    
    public boolean modified() {
        return classLoader != null ? classLoader.modified() : false ;
    }

WebappClassLoader:

    public boolean modified() {
        int length = paths.length;
        //最后一次文件修改时间的列表
        int length2 = lastModifiedDates.length;
        //如果当前文件夹内容多了,则使用久的文件夹长度遍历(避免数组越界)
        if (length > length2)
            length = length2;
        //遍历当前war包文件夹的所有文件
        for (int i = 0; i < length; i++) {
            try {
                //拿到当前文件的修改时间
                long lastModified =
                    ((ResourceAttributes) resources.getAttributes(paths[i]))
                    .getLastModified();
                //和该文件上一次的修改时间对比,存在一个不同则为已修改
                if (lastModified != lastModifiedDates[i]) {
                    return (true);
                }
            }
        }

        length = jarNames.length;
        //判断war包是否被删除
        if (getJarPath() != null) {
            try {
                //获取war包中文件列表
                NamingEnumeration<Binding> enumeration =
                    resources.listBindings(getJarPath());
                int i = 0;
                while (enumeration.hasMoreElements() && (i < length)) {
                    NameClassPair ncPair = enumeration.nextElement();
                    String name = ncPair.getName();
                    //不是jar包则跳过
                    if (!name.endsWith(".jar"))
                        continue;
                    //如果jar包列表对不上,则表示jar包发生变化
                    if (!name.equals(jarNames[i])) {
                        return (true);
                    }
                    i++;
                }
                if (enumeration.hasMoreElements()) {
                    while (enumeration.hasMoreElements()) {
                        NameClassPair ncPair = enumeration.nextElement();
                        String name = ncPair.getName();
                        //添加了新的jar包也是已修改
                        if (name.endsWith(".jar")) {
                            return (true);
                        }
                    }
                } else if (i < jarNames.length) {
                    return (true);
                }
            }
            //...省略

        }
        return (false);

    }

StandardContext:
    public synchronized void reload() {
        if (!getState().isAvailable())
            throw new IllegalStateException
                (sm.getString("standardContext.notStarted", getName()));
       
        setPaused(true);
        //停止容器
        stop();
        //再启动容器
        start();
        
        setPaused(false);

    }

    protected synchronized void stopInternal() throws LifecycleException {
        //... 其它内容省略
        //拿到旧的类加载器
        ClassLoader old = bindThread();
        //解绑类加载器
        unbindThread(old);
        if ((loader != null) && (loader instanceof Lifecycle)) {
            //...停止类加载器,即清除所有的引用,下次加载类时则会重新加载
            ((Lifecycle) loader).stop();
        }
    }

在上述代码中,我们可以看到Tomcat是通过对比classes和lib文件夹中的jar包来判断是否发生了变化,主要是判断classes文件夹是否减少、或者发生修改;判断lib文件夹下的jar包是否新增、减少;如果发生了上述情况则属于修改,然后重新加载当前Context应用容器,并清理当前Context的类加载器中的所有引用,下次加载类时则重新加载,从而实现了热加载机制。

热加载总结:

在Tomcat的后台定时任务中,如果开启了热加载,会检测classes文件夹下和lib下的jar包的内容是否发生了变化,如果发生了变化则会重新加载Context应用容器,清除对应的类加载器的引用,从而实现热加载,因为会遍历classes下的所有文件,所以很消耗资源,线上环境最好别开启,开发时使用即可。

相关推荐
codeMaster__hyd15 小时前
CentOS7系统下部署tomcat,浏览器访问localhost:8080/
服务器·阿里云·tomcat
Monly211 天前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
Ttang231 天前
Tomcat原理(6)——tomcat完整实现
java·tomcat
梁萌2 天前
Docker快速安装Tomcat
docker·容器·tomcat·镜像
全栈老实人_3 天前
时间管理系统|Java|SSM|JSP|
java·开发语言·tomcat·maven
2401_850410833 天前
LVS简介
运维·nginx·tomcat·lvs
程序员大金3 天前
基于SSM+Vue的个性化旅游推荐系统
前端·vue.js·mysql·java-ee·tomcat·mybatis·旅游
第八学期3 天前
Tomcat快速入门(Java环境介绍+Tomcat快速安装+Tomcat配置文件+Tomcat配置虚拟主机+Tomcat管理界面)
java·运维·开发语言·tomcat
Ttang233 天前
Tomcat原理(4)——尝试手动Servlet的实现
java·开发语言·servlet·java-ee·tomcat·intellij-idea
小佟3 天前
Tomcat10安装报错Unknown module: java.rmi specified to --add-opens
tomcat