美文网首页
Tomcat War 包部署源码分析

Tomcat War 包部署源码分析

作者: 绝尘驹 | 来源:发表于2020-05-24 21:33 被阅读0次

    这偏文章是为了弄明白一个问题,就是tomcat部署war包的时候是部署已经解压过多目录文件还是部署war包。

    tomcat启动部署的整个来龙去脉,要理清楚,先要从StandandHost的初始化开始。

    StandandHost 在启动执行start的时候,自己没有做什么事情,关键的代码在父类ContainerBase的startInternal方法如下的代码:

     setState(LifecycleState.STARTING);
    

    LifecycleState.STARTING为START_EVENT 事件,StandandHost有一个事件监听器HostConfig,setState方法会通知HostConfig执行start方法。

    HostConfig的start方法如下:

      public void start() {
    
        if (log.isDebugEnabled())
            log.debug(sm.getString("hostConfig.start"));
    
        try {
            ObjectName hostON = host.getObjectName();
            oname = new ObjectName
                (hostON.getDomain() + ":type=Deployer,host=" + host.getName());
            Registry.getRegistry(null, null).registerComponent
                (this, oname, this.getClass().getName());
        } catch (Exception e) {
            log.warn(sm.getString("hostConfig.jmx.register", oname), e);
        }
    
        if (!host.getAppBaseFile().isDirectory()) {
            log.error(sm.getString("hostConfig.appBase", host.getName(),
                    host.getAppBaseFile().getPath()));
            host.setDeployOnStartup(false);
            host.setAutoDeploy(false);
        }
    
        if (host.getDeployOnStartup())
            deployApps();
    
    }
    

    如果host的deployOnStartup 属性默认为true,所以tomcat启动的时候就是部署webapps目录下的应用,下面我们看下tomcat的部署顺序,因为webapps目录下可有war包文件,应用的目录文件等,先部署那个,或者两个都存在的情况下,tomcat是怎么处理的,我们经常遇到webapps目录下即有一个app的war包,也有对应的目录。

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

    通过上面的代码可以看出,tomcat 开始部署时,先部署descriptors,在部署war包,最后是目录,下面看下面具体每个部署是怎么进行的

    并发部署

    我们看下上面deployWARs方法的代码如下,参数appBase是webapps的路径,files是该目录下的文件

    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++) {
            //过滤掉META-INF和WEB-INF文件
            if (files[i].equalsIgnoreCase("META-INF"))
                continue;
            if (files[i].equalsIgnoreCase("WEB-INF"))
                continue;
            File war = new File(appBase, files[i]);
            //如果是war包文件,则继续处理
            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;
                }
    
                // Check for WARs with /../ /./ or similar sequences in the name
                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)));
            }
        }
        //等待服务启动完成。
        for (Future<?> result : results) {
            try {
                result.get();
            } catch (Exception e) {
                log.error(sm.getString(
                        "hostConfig.deployWar.threaded.error"), e);
            }
        }
    }
    

    上面的代码有点长,主要是过滤掉哪些不需要的,或者已经部署好了的war包应用,如果符合条件,就提交一个任务到线程池去执行,这里个注意的部署应用线程池线程的个数,默认是1,通过内部实现的InlineExecutorService来执行,则是同步启动, 通过startStopThreads设置,如果大于1则可以真正并行部署。

    创建StandardContext

    DeployWar 任务的逻辑在HostConfig的deployWAR方法,这个方法比较长,就不贴代码了,关键点是tomcat为每个war包会创建一个对应的StandardContext,并设置对应的listener为ContextConfig,这个ContextConfig是固定的,代码如下:

          //指定standardContext 的 contextConfig,后面部署war包时会不用到
            Class<?> clazz = Class.forName(host.getConfigClass());
            LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
            context.addLifecycleListener(listener);
    
            context.setName(cn.getName());
            context.setPath(cn.getPath());
            context.setWebappVersion(cn.getVersion());
            context.setDocBase(cn.getBaseName() + ".war");
            //开始触发StandardContext的初始化
            host.addChild(context);
    

    解压WAR包

    一个StandardContext 对应一个服务实例,要启动服务,就需要先解压war吧,这个逻辑在StandardContext的ContextConfig实现

    StandardContext在start前,会产生一个before 事件,ContextConfig会根据事件执行启动前,启动后相关的逻辑。

    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
            configureStart();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
            beforeStart();
    }
    

    ContextConfig 有两个重要的事件,对应configureStart和beforeStart,
    beforeStart是初始化StandardContext前调用的, 即解压war包,configureStart是StandardContext初始化好后,调用的,是解析web xml 文件的入口,这里beforeStart()的就是调用了fixDocBase方法,核心逻辑在fixDocBase里面,核心代码就是判断是否解压,执行解压,如下:

    protected void fixDocBase() throws IOException {
    
        // At this point we need to determine if we have a WAR file in the
        // appBase that needs to be expanded. Therefore we consider the absolute
        // docBase NOT the canonical docBase. This is because some users symlink
        // WAR files into the appBase and we want this to work correctly.
        boolean docBaseAbsoluteInAppBase = docBaseAbsolute.startsWith(appBase.getPath() + File.separatorChar);
        if (docBaseAbsolute.toLowerCase(Locale.ENGLISH).endsWith(".war") && !docBaseAbsoluteFile.isDirectory()) {
            URL war = UriUtil.buildJarUrl(docBaseAbsoluteFile);
            if (unpackWARs) {
                docBaseAbsolute = ExpandWar.expand(host, war, pathName);
                docBaseAbsoluteFile = new File(docBaseAbsolute);
                if (context instanceof StandardContext) {
                    ((StandardContext) context).setOriginalDocBase(originalDocBase);
                }
            } else {
                ExpandWar.validate(host, war, pathName);
            }
        } 
    }
    

    通过ExpandWar.expand方法去解压war包,expand 才是最终解压war包的地方,而且要不要解压都在这里,下面是检查是否要解压的代码,解压部分代码去掉了

    public static String expand(Host host, URL war, String pathname)
        throws IOException {
    
        /* Obtaining the last modified time opens an InputStream and there is no
         * explicit close method. We have to obtain and then close the
         * InputStream to avoid a file leak and the associated locked file.
         */
        JarURLConnection juc = (JarURLConnection) war.openConnection();
        juc.setUseCaches(false);
        URL jarFileUrl = juc.getJarFileURL();
        URLConnection jfuc = jarFileUrl.openConnection();
    
        boolean success = false;
        File docBase = new File(host.getAppBaseFile(), pathname);
        File warTracker = new File(host.getAppBaseFile(), pathname + Constants.WarTracker);
        long warLastModified = -1;
    
        try (InputStream is = jfuc.getInputStream()) {
            // Get the last modified time for the WAR
            warLastModified = jfuc.getLastModified();
        }
    
        //如果war包文件对应的目录也存在,则检查对应目录下的/META-INF/warTracker文件的修改日期。
        // Check to see of the WAR has been expanded previously
        if (docBase.exists()) {
            // A WAR was expanded. Tomcat will have set the last modified
            // time of warTracker file to the last modified time of the WAR so
            // changes to the WAR while Tomcat is stopped can be detected
            //如果不存在,或者日期和war包文件的修改时间相等,则直接返回。
            if (!warTracker.exists() || warTracker.lastModified() == warLastModified) {
                // No (detectable) changes to the WAR
                success = true;
                return docBase.getAbsolutePath();
            }
    
            // WAR must have been modified. Remove expanded directory.
            log.info(sm.getString("expandWar.deleteOld", docBase));
            //删除应用对应的目录,需要重新解压。
            if (!delete(docBase)) {
                throw new IOException(sm.getString("expandWar.deleteFailed", docBase));
            }
        }
    
        // Create the new document base directory
        if(!docBase.mkdir() && !docBase.isDirectory()) {
            throw new IOException(sm.getString("expandWar.createFailed", docBase));
        }
    
        // Expand the WAR into the new document base directory
        String canonicalDocBasePrefix = docBase.getCanonicalPath();
        if (!canonicalDocBasePrefix.endsWith(File.separator)) {
            canonicalDocBasePrefix += File.separator;
        }
    
        // Creating war tracker parent (normally META-INF)
        File warTrackerParent = warTracker.getParentFile();
        if (!warTrackerParent.isDirectory() && !warTrackerParent.mkdirs()) {
            throw new IOException(sm.getString("expandWar.createFailed", warTrackerParent.getAbsolutePath()));
        }
    
        
            // Create the warTracker file and align the last modified time
            // with the last modified time of the WAR
            if (!warTracker.createNewFile()) {
                throw new IOException(sm.getString("expandWar.createFileFailed", warTracker));
            }
            if (!warTracker.setLastModified(warLastModified)) {
                throw new IOException(sm.getString("expandWar.lastModifiedFailed", warTracker));
            }
    
            success = true;
        } catch (IOException e) {
            throw e;
        } finally {
            if (!success) {
                // If something went wrong, delete expanded dir to keep things
                // clean
                deleteDir(docBase);
            }
        }
    
        // Return the absolute path to our new document base directory
        return docBase.getAbsolutePath();
    }
    

    warTracker 文件

    tomcat 解压war时,会生成一个warTracker文件,在对应服务目录下的/META-INF/目录下,并设置修改时间,后面再部署时,通过检查warTracker 这个文件的修改时间,查看war包是否有变更。

    是否需要解压war包

    通过判断docBase.exists(),即war包对应的目录是否存在,我们平时只要部署过一次,war包下面的文件是存在的,即已经存在了,就根据warTracker不存在,即有可能被删除了,也不解压,如果存在则比较war包和warTracker的修改时间,如果相等,则不解压,代表war包没有变化,总结就是只有在warTracker存在而且war包的修改时间有变化的情况下,比如我们重新打了war包,放到这里,就会用新的war包部署,如果你把原来目录的warTracker删了,那也不会部署新的了。

    WebAppClassload准备

    启动Listener和Servlet

    应用的文件准备好了,应用的webappclassload也准备好了后,可以开始解析web应用的标准启动文件web.xml了,也就是上面提到的ContextConfig的configureStart方法,这里主要是有webConfig方法实现,这里不做详细分析,主要说下tomcat对servlet的解析,tomcat 在解析完web.xml后,会配置context,servlet配置代码如下:

    for (ServletDef servlet : webxml.getServlets().values()) {
            Wrapper wrapper = context.createWrapper();
            // Description is ignored
            // Display name is ignored
            // Icons are ignored
    
            // jsp-file gets passed to the JSP Servlet as an init-param
    
            if (servlet.getLoadOnStartup() != null) {
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            if (servlet.getEnabled() != null) {
                wrapper.setEnabled(servlet.getEnabled().booleanValue());
            }
            wrapper.setName(servlet.getServletName());
            Map<String,String> params = servlet.getParameterMap();
            for (Entry<String, String> entry : params.entrySet()) {
                wrapper.addInitParameter(entry.getKey(), entry.getValue());
            }
            wrapper.setRunAs(servlet.getRunAs());
            Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
            for (SecurityRoleRef roleRef : roleRefs) {
                wrapper.addSecurityReference(
                        roleRef.getName(), roleRef.getLink());
            }
            wrapper.setServletClass(servlet.getServletClass());
            MultipartDef multipartdef = servlet.getMultipartDef();
            if (multipartdef != null) {
                if (multipartdef.getMaxFileSize() != null &&
                        multipartdef.getMaxRequestSize()!= null &&
                        multipartdef.getFileSizeThreshold() != null) {
                    wrapper.setMultipartConfigElement(new MultipartConfigElement(
                            multipartdef.getLocation(),
                            Long.parseLong(multipartdef.getMaxFileSize()),
                            Long.parseLong(multipartdef.getMaxRequestSize()),
                            Integer.parseInt(
                                    multipartdef.getFileSizeThreshold())));
                } else {
                    wrapper.setMultipartConfigElement(new MultipartConfigElement(
                            multipartdef.getLocation()));
                }
            }
            if (servlet.getAsyncSupported() != null) {
                wrapper.setAsyncSupported(
                        servlet.getAsyncSupported().booleanValue());
            }
            wrapper.setOverridable(servlet.isOverridable());
            context.addChild(wrapper);
        }
    

    可以看到,tomcat对每个servlet的创建一个了StandardWrapper的实例,并设置我们配置的相关参数,你应该很眼熟。

    我们用到的listener,servlet,filter 这些都设置完了后,就要用我们的webappclassload来加载这些类了,这些代码在StandardContext 初始化方法startInternal结尾实现,代码如下:

            // Configure and call application event listeners
            if (ok) {
                //初始化我们定义的listener在web.xml里面
                if (!listenerStart()) {
                    log.error(sm.getString("standardContext.listenerFail"));
                    ok = false;
                }
            }
    
            // Check constraints for uncovered HTTP methods
            // Needs to be after SCIs and listeners as they may programmatically
            // change constraints
            if (ok) {
                checkConstraintsForUncoveredMethods(findConstraints());
            }
    
            try {
                // Start manager
                Manager manager = getManager();
                if (manager instanceof Lifecycle) {
                    ((Lifecycle) manager).start();
                }
            } catch(Exception e) {
                log.error(sm.getString("standardContext.managerFail"), e);
                ok = false;
            }
    
            // Configure and call application filters
            if (ok) {
                //初始化我们定义的filter在web.xml里面
                if (!filterStart()) {
                    log.error(sm.getString("standardContext.filterFail"));
                    ok = false;
                }
            }
    
            // Load and initialize all "load on startup" servlets
            if (ok) {
                //初始化我们定义的Servlet在web.xml里面
                if (!loadOnStartup(findChildren())){
                    log.error(sm.getString("standardContext.servletFail"));
                    ok = false;
                }
            }
    

    我们平时在web.xml里面都会有listener,servlet,filter,listener在启动的时候就会创建实例,比如spring上下文的初始化,这个没有疑问,servlet的就是通过配置指定即loadOnStartup的配置,如果小于0则运行时再创建实例,否则都会在初始化启动的时候就创建实例。

    总结

    弄明白一个问题,又写了这么多,tomcat 启动部署web应用时,先部署war包文件,如果对应的目录下有warTrack 文件而且两者的更新时间是一样的,则不解压,直接用已经解压过的目录文件部署。否则删掉老的目录,重新解压,同时支持并行部署,默认是一个线程,可以通过配置多个线程实现并行部署

    war包部署完后,开始部署目录的服务,如果war包已经部署过的,肯定就不执行了,只是部署哪些没有war包文件的应用这里就不研究了。

    相关文章

      网友评论

          本文标题:Tomcat War 包部署源码分析

          本文链接:https://www.haomeiwen.com/subject/cotufhtx.html