Tomcat War 包部署源码分析

2020-05-24  本文已影响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包文件的应用这里就不研究了。

上一篇下一篇

猜你喜欢

热点阅读