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();
    }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容