Tomcat启动分析(九) - Host组件

Host组件表示一个虚拟主机,在Host元素内可以有多个Context元素与之关联以表示不同的Web应用,Engine元素内可以配置多个Host,但其中一个的名称必须与Engine的defaultHost属性值相匹配。

Host组件

Host接口继承Container接口,StandardHost类是Host组件的默认实现,它继承ContainerBase基类并实现了Host接口,其构造函数和部分成员变量如下所示:

public class StandardHost extends ContainerBase implements Host {
    // ----------------------------------------------------------- Constructors
    /**
     * Create a new StandardHost component with the default basic Valve.
     */
    public StandardHost() {
        super();
        pipeline.setBasic(new StandardHostValve());

    }
    // ----------------------------------------------------- Instance Variables
    /**
     * The set of aliases for this Host.
     */
    private String[] aliases = new String[0];

    private final Object aliasesLock = new Object();

    /**
     * The application root for this Host.
     */
    private String appBase = "webapps";
    private volatile File appBaseFile = null;

    /**
     * The XML root for this Host.
     */
    private String xmlBase = null;

    /**
     * host's default config path
     */
    private volatile File hostConfigBase = null;

    /**
     * The auto deploy flag for this Host.
     */
    private boolean autoDeploy = true;

    /**
     * The Java class name of the default context configuration class
     * for deployed web applications.
     */
    private String configClass =
        "org.apache.catalina.startup.ContextConfig";

    /**
     * The Java class name of the default Context implementation class for
     * deployed web applications.
     */
    private String contextClass = "org.apache.catalina.core.StandardContext";

    /**
     * The deploy on startup flag for this Host.
     */
    private boolean deployOnStartup = true;

    /**
     * deploy Context XML config files property.
     */
    private boolean deployXML = !Globals.IS_SECURITY_ENABLED;

    /**
     * Should XML files be copied to
     * $CATALINA_BASE/conf/<engine>/<host> by default when
     * a web application is deployed?
     */
    private boolean copyXML = false;

    /**
     * The Java class name of the default error reporter implementation class
     * for deployed web applications.
     */
    private String errorReportValveClass =
        "org.apache.catalina.valves.ErrorReportValve";

    /**
     * Unpack WARs property.
     */
    private boolean unpackWARs = true;

    /**
     * Work Directory base for applications.
     */
    private String workDir = null;

    /**
     * Should we create directories upon startup for appBase and xmlBase
     */
    private boolean createDirs = true;

    /**
     * Track the class loaders for the child web applications so memory leaks
     * can be detected.
     */
    private final Map<ClassLoader, String> childClassLoaders =
            new WeakHashMap<>();

    /**
     * Any file or directory in {@link #appBase} that this pattern matches will
     * be ignored by the automatic deployment process (both
     * {@link #deployOnStartup} and {@link #autoDeploy}).
     */
    private Pattern deployIgnore = null;
    private boolean undeployOldVersions = false;
    private boolean failCtxIfServletStartFails = false;

    @Override
    public void addChild(Container child) {
        child.addLifecycleListener(new MemoryLeakTrackingListener());
        if (!(child instanceof Context))
            throw new IllegalArgumentException
                (sm.getString("standardHost.notContext"));
        super.addChild(child);
    }
    // 省略一些代码
}

重要的成员变量如下:

  • appBase表示本Host的Web应用根路径;
  • xmlBase表示本Host的XML根路径;
  • autoDeploy表示是否应周期性地部署新的或者更新过的Web应用;
  • configClass和contextClass分别表示部署Web应用时Context组件的配置类和实现类;
  • deployOnStartup表示Tomcat启动时是否自动部署本Host的应用;
  • unpackWARs表示是否要将WAR文件解压成目录;
  • createDirs表示启动阶段是否要创建appBase和xmlBase表示的目录;
  • deployIgnore表示当autoDeploy和deployOnStartup属性被设置时忽略的文件模式;
  • 其余变量的含义可以参考Host的配置文档

Host组件的构造函数为自己的Pipeline添加基本阀StandardHostValve,addChild方法只能添加Context组件。

组件初始化

StandardHost类并没有重写initInternal方法,因此它的初始化过程只是为自己创建了一个线程池用于启动和停止自己的子容器。

组件启动

StandardHost类的startInternal方法如下所示:

@Override
protected synchronized void startInternal() throws LifecycleException {
    // Set error report valve
    String errorValve = getErrorReportValveClass();
    if ((errorValve != null) && (!errorValve.equals(""))) {
        try {
            boolean found = false;
            Valve[] valves = getPipeline().getValves();
            for (Valve valve : valves) {
                if (errorValve.equals(valve.getClass().getName())) {
                    found = true;
                    break;
                }
            }
            if(!found) {
                Valve valve =
                    (Valve) Class.forName(errorValve).getConstructor().newInstance();
                getPipeline().addValve(valve);
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString(
                    "standardHost.invalidErrorReportValveClass",
                    errorValve), t);
        }
    }
    super.startInternal();
}
  • 如果所关联的Pipeline中没有错误报告阀(errorReportValveClass),那么就给Pipeline添加一个,否则什么也不做;
  • 调用基类ContainerBase类的startInternal方法,先启动子容器组件,然后启动Pipeline,最后发布LifecycleState.STARTING事件给添加到Host组件自身的生命周期事件监听器。

HostConfig监听器

Host组件里比较重要的生命周期事件监听器之一是HostConfig监听器,Tomcat在解析server.xml时为Digester添加了HostRuleSet规则,进而为StandardHost添加HostConfig生命周期监听器(请参见本系列的Tomcat启动分析(二))。HostConfig的主要作用是在Host组件启动时响应Host发布的事件。

成员变量

HostConfig类实现了生命周期事件监听器接口LifecycleListener,成员变量如下所示:

public class HostConfig implements LifecycleListener {
    private static final Log log = LogFactory.getLog(HostConfig.class);
    protected static final StringManager sm = StringManager.getManager(HostConfig.class);

    /**
     * The resolution, in milliseconds, of file modification times.
     */
    protected static final long FILE_MODIFICATION_RESOLUTION_MS = 1000;
    protected String contextClass = "org.apache.catalina.core.StandardContext";

    /**
     * The Host we are associated with.
     */
    protected Host host = null;
    protected ObjectName oname = null;
    protected boolean deployXML = false;
    protected boolean copyXML = false;
    protected boolean unpackWARs = false;

    /**
     * Map of deployed applications.
     */
    protected final Map<String, DeployedApplication> deployed =
            new ConcurrentHashMap<>();

    public boolean isCopyXML() {
        return (this.copyXML);
    }

    public void setCopyXML(boolean copyXML) {
        this.copyXML= copyXML;
    }

    public boolean isUnpackWARs() {
        return (this.unpackWARs);
    }

    public void setUnpackWARs(boolean unpackWARs) {
        this.unpackWARs = unpackWARs;
    }
}
  • host变量引用该监听器关联的Host组件;
  • 其他成员变量如deployXML、copyXML和unpackWARs等与Host组件的对应变量含义相同。

响应生命周期事件

HostConfig类实现的lifecycleEvent方法如下:

@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();
    }
}
  • 首先将发布事件的Host组件的copyXML、deployXML、unpackWARs和contextClass属性绑定到该监听器自身;
  • 然后针对不同事件执行不同的事件处理方法,以启动事件Lifecycle.START_EVENT为例,当启动事件到来时start方法被调用,若Host组件设置了deployOnStartup则部署Web应用。
    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.error(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();
    }
    

部署Web应用

部署Web应用是由deployApps方法完成的,其代码如下所示,内部首先调用filterAppPaths过滤掉与deployIgnore模式匹配的文件,然后才部署剩余的应用。

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

下面以部署WAR文件为例分析Web应用是如何被部署的。

部署WAR文件

部署WAR文件是由deployWARs方法完成的,其代码如下所示:

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;
            }
            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文件、XML文件或者目录都对应一个Context,根据它们的文件名可以得到基本文件名、Context名称、Context版本和Context路径,详见Context配置文档的Naming一节。
对appBase目录下的每个没被过滤掉的且文件名以.war结尾的文件:

  • 如果文件名忽略大小写后与META-INF或者WEB-INF相等则跳过不处理;
  • 如果该文件对应的Context已经开始服务或者已经在Host组件里部署,那么跳过不处理;
  • 如果该文件对应的Context路径包含非法字符,那么跳过不处理;
  • 在Host组件的startStopExecutor中部署符合要求的WAR文件,startStopExecutor的作用请见上一篇文章

部署Web应用时,关于文件名有以下几点需要注意:

  • 文件名不能包含*和?两个字符(Servlet规范并没有这个要求),因为它们会被JMX认成通配符模式。如果使用则会报与JMX相关的错误,可参考ObjectName文档
  • 除了*和?两个字符外,其他字符(包括中文字符)均可以作为文件名的一部分。

为Web应用创建Context

上文提到HostConfig会在与之关联的Host组件的startStopExecutor中部署符合要求的WAR文件,这是通过HostConfig的静态内部类DeployWar实现的,该类实现了Runnable接口,在run方法中调用了HostConfig类自身的deployWAR方法,该方法部分代码如下所示:

/**
 * Deploy packed WAR.
 * @param cn The context name
 * @param war The WAR file
 */
protected void deployWAR(ContextName cn, File war) {
    File xml = new File(host.getAppBaseFile(),
            cn.getBaseName() + "/" + Constants.ApplicationContextXml);

    // 省略部分代码

    Context context = null;
    boolean deployThisXML = isDeployThisXML(war, cn);
    try {
        if (deployThisXML && useXml && !copyXML) {
            // 省略部分代码
        } else if (deployThisXML && xmlInWar) {
            // 省略部分代码
        } else if (!deployThisXML && xmlInWar) {
            // 省略部分代码
        } else {
            context = (Context) Class.forName(contextClass).getConstructor().newInstance();
        }
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.error(sm.getString("hostConfig.deployWar.error",
                war.getAbsolutePath()), t);
    } finally {
        if (context == null) {
            context = new FailedContext();
        }
    }

    // 省略部分代码

    DeployedApplication deployedApp = new DeployedApplication(cn.getName(),
            xml.exists() && deployThisXML && copyThisXml);

    long startTime = 0;
    // Deploy the application in this WAR file
    if(log.isInfoEnabled()) {
        startTime = System.currentTimeMillis();
        log.info(sm.getString("hostConfig.deployWar",
                war.getAbsolutePath()));
    }
    try {
        // Populate redeploy resources with the WAR file
        deployedApp.redeployResources.put
            (war.getAbsolutePath(), Long.valueOf(war.lastModified()));

        if (deployThisXML && xml.exists() && copyThisXml) {
            deployedApp.redeployResources.put(xml.getAbsolutePath(),
                    Long.valueOf(xml.lastModified()));
        } else {
            // In case an XML file is added to the config base later
            deployedApp.redeployResources.put(
                    (new File(host.getConfigBaseFile(),
                            cn.getBaseName() + ".xml")).getAbsolutePath(),
                    Long.valueOf(0));
        }
        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");
        host.addChild(context);
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.error(sm.getString("hostConfig.deployWar.error",
                war.getAbsolutePath()), t);
    } finally {
        // 省略部分代码
    }
    deployed.put(cn.getName(), deployedApp);
    if (log.isInfoEnabled()) {
        log.info(sm.getString("hostConfig.deployWar.finished",
            war.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));
    }
}

该方法有以下几点需要注意:

  • 对一般WAR文件部署的过程,与该文件对应的Context对象会被创建,具体的类型是HostConfig的contextClass属性表示的类型,默认是StandardContext类型;
  • 在为Context对象设置其他属性如名称、路径和版本等之后,Host组件通过addChild方法将该Context对象加入到自己的子容器组件集合中。在ContainerBase类的addChild方法可以看到,添加之后便启动了子容器组件。

调试部署WAR文件

为了方便调试部署WAR文件的流程,可以使用嵌入式Tomcat。调试的基本代码如下,指定WAR文件所在目录即可。

import org.apache.catalina.Host;
import org.apache.catalina.startup.HostConfig;
import org.apache.catalina.startup.Tomcat;

public class DebugTomcat {

    public static void main(String[] args) throws Exception {
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8081);
        Host host = tomcat.getHost();
        host.setAppBase("path for directory with wars"); // WAR文件的路径
        host.addLifecycleListener(new HostConfig());
        tomcat.start();
        tomcat.getServer().await();
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容