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下的所有文件,所以很消耗资源,线上环境最好别开启,开发时使用即可。

相关推荐
尢词21 小时前
SpringMVC
java·spring·java-ee·tomcat·maven
清风百草21 小时前
【04】【Maven项目热部署】将Maven项目热部署到远程tomcat服务器上
tomcat·maven项目热部署
蒋桐城1 天前
Tomcat 启动卡住,日志显示 At least one JAR was scanned for TLDs yet contained no TLDs.
java·tomcat
qiaosaifei1 天前
SpringBoot项目中替换指定版本的tomcat
spring boot·后端·tomcat
雷神乐乐2 天前
IDEA构建JavaWeb项目,并通过Tomcat成功运行
服务器·tomcat·javaweb
陈大爷(有低保)2 天前
数据库连接池JNDI
数据库·mysql·tomcat
笔墨登场说说2 天前
JDK 里面的线程池和Tomcat线程池的区别
java·servlet·tomcat
爱分享的淘金达人2 天前
25国考照片处理器使用流程图解❗
java·考研·spring·eclipse·tomcat
爱分享的淘金达人2 天前
2025年山东省考报名流程图解
java·考研·spring·eclipse·tomcat·流程图
弓弧名家_玄真君3 天前
mac 安装tomcat
java·macos·tomcat