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

2018-12-11  本文已影响0人  buzzerrookie

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);
    }
    // 省略一些代码
}

重要的成员变量如下:

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

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

响应生命周期事件

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

部署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结尾的文件:

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

为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文件

为了方便调试部署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();
    }
}
上一篇 下一篇

猜你喜欢

热点阅读