【Tomcat】Tomcat原始碼閱讀之StandardHost與HostConfig的分析

雲川之下發表於2020-12-20


前面的文章分析了 StandardEngine,這裡來分析一下由 Engine物件來管理的另外一種物件吧: Host

StandardHost物件的建立

一般情況下,在tomcat中都是預設使用StandardHost型別。。。這裡先來看看在catalina是如何配置建立StandardHost物件的吧:

//建立host物件
        digester.addObjectCreate(prefix + "Host",
                                 "org.apache.catalina.core.StandardHost",  //建立host物件的配置
                                 "className");
        digester.addSetProperties(prefix + "Host");
        digester.addRule(prefix + "Host",
                         new CopyParentClassLoaderRule());  //會將host的parentClassloader設定為engine的,engine被設定為sharedloader
        digester.addRule(prefix + "Host",   //為host設定配置的監聽
                         new LifecycleListenerRule
                         ("org.apache.catalina.startup.HostConfig",   //這個算是比較重要的吧,在裡面會具體的建立context啥的
                          "hostConfigClass"));
        digester.addSetNext(prefix + "Host",
                            "addChild",
                            "org.apache.catalina.Container");  // 在engine上面呼叫addChild方法,用於新增當前的host到engine上面去

這部分具體的涉及到Host物件的建立,第一個是具體的建立要用的host物件,也就是StandardHost物件,接著是指定為host設定的parentclassLoader為engine的parentClassLoader,在前面的文章中,我們知道engine的parentClassLoader會被設定為sharedLoader,所以這裡host物件的parentClassLoader也會被設定為sharedLoader。。

然後還有比較重要的配置項吧,為StandardHost物件新增lifecycle的監聽器,為HostConfig型別的物件。。它是比較重要的吧,因為它會具體的負責對context的建立,啟動啥的。。這個一會再來說吧。。

然後就最後呼叫engine的addChild方法將當前host物件新增到engine上面去了。。。

host介面

好啦,到這裡怎樣建立host物件應該算是知道了吧。。。那麼接下來來看看host介面是如何定義的吧:

public interface Host extends Container {
 
	//host的一些事件的定義,新增別名,移除別名啥的
    public static final String ADD_ALIAS_EVENT = "addAlias";
 
    public static final String REMOVE_ALIAS_EVENT = "removeAlias";
 
    public String getXmlBase(); //當前host物件的配置檔案的路徑,這個檔案不一定存在吧   /conf/enginename/hostname/
 
    public void setXmlBase(String xmlBase);
 
    public File getConfigBaseFile();  //當前host的配置xml檔案
 
    public String getAppBase();  //當前host的app在什麼地方
 
    public File getAppBaseFile();   //獲取app的放的目錄的檔案引用 
 
    public void setAppBase(String appBase);   //設定app存放的路徑
 
    public boolean getAutoDeploy();  //是否自動部署
 
    public void setAutoDeploy(boolean autoDeploy);   //是否自動部署
 
    public String getConfigClass();   //用於監聽context的listener的型別
 
    public void setConfigClass(String configClass);   
 
    public boolean getDeployOnStartup();  //啟動的時候部署?
 
    public void setDeployOnStartup(boolean deployOnStartup);   
 
 
    public String getDeployIgnore();
 
    public Pattern getDeployIgnorePattern();  //context名字匹配用的正規表示式
 
    public void setDeployIgnore(String deployIgnore);
 
 
    public ExecutorService getStartStopExecutor();  //用於啟動和停止子container(也就是context)的executor
 
    public boolean getCreateDirs();    //如果是ture的話,那麼會嘗試為應用程式和host的配置建立資料夾
 
    public void setCreateDirs(boolean createDirs);
 
    public boolean getUndeployOldVersions();  //是否自動解除安裝程式的老版本
 
    public void setUndeployOldVersions(boolean undeployOldVersions);
 
    public void addAlias(String alias);   //為當前host新增別名
 
    public String[] findAliases();  //獲取當前host的所有別名
 
    public void removeAlias(String alias);   //移除一個別名
}

介面定義稍微長一些吧,不過也還挺簡單的,主要是一些配置,別名什麼的管理,這個具體看上面的註釋應該能比較的清楚吧。

StandardHost

好啦,接下來來看看StandardHost是怎麼實現的吧,先來看看簡單的繼承體系:
在這裡插入圖片描述
這個應該算是很簡單的吧,首先也是一個容器。。。然後實現了host介面。。。這裡來看看它的建構函式吧:

    public StandardHost() {
 
        super();
        pipeline.setBasic(new StandardHostValve());  //設定basic
 
    }

沒做什麼事情吧,無非是在pipeline上面新增了一個basic的valve物件。。。接下來再來看看一些重要的屬性的定義吧:

    private String[] aliases = new String[0];  //當前host物件的別名的陣列
 
    private final Object aliasesLock = new Object();  //鎖
 
    private String appBase = "webapps";   //預設的app的路徑是tomcat根路徑下的webapps資料夾
    private volatile File appBaseFile = null;   //引用這個資料夾
 
    private String xmlBase = null;   //xml配置檔案所在的目錄
 
    private volatile File hostConfigBase = null;   //host的預設配置路徑   conf/ + enginename + / + hostname 
 
    private boolean autoDeploy = true;  //預設是自動部署的
 
    private String configClass =
        "org.apache.catalina.startup.ContextConfig";   // 預設的config型別,是個listener,通過監聽當前host的狀態來部署context啥的
 
    private String contextClass =
        "org.apache.catalina.core.StandardContext";   //預設用到的context物件的型別
 
    private boolean deployOnStartup = true;  //預設在啟動的時候部署應用
 
    private boolean deployXML = !Globals.IS_SECURITY_ENABLED;
 
    private boolean copyXML = false;  //
 
    private String errorReportValveClass =
        "org.apache.catalina.valves.ErrorReportValve";  //用於儲存的valve預設的型別
 
    private boolean unpackWARs = true;   //預設要解壓war包
    private String workDir = null;    //app的work路徑
 
    private boolean createDirs = true;  //預設在啟動的時候建立資料夾
 
    private final Map<ClassLoader, String> childClassLoaders =  //跟蹤每個app的classLoaer,用於定位記憶體洩露
            new WeakHashMap<>();
 
    private Pattern deployIgnore = null;
 
 
    private boolean undeployOldVersions = false;  //預設不解除安裝老版本

嗯,具體這些屬性的用處在註釋上應該比較的清楚了。。其實host物件本身無非就是對這些屬性的管理。。自己並沒有太多的要做的事情。。方法也基本上都是對這些屬性的設定什麼的。。。這裡就不具體的來分析這些方法了,有興趣自己看看就是了。。挺簡單的。。。

那麼這裡來看看host物件的pipeline上的basic的valve幹了的invoke做了什麼事情吧。。我們在前面知道。。在engine的basic的vavle上將會呼叫請求所屬的host的pipeline來處理請求。。

    //其實這裡主要是是呼叫當前請求的context的pipeline來處理
    public final void invoke(Request request, Response response)
        throws IOException, ServletException {
        // Select the Context to be used for this Request
        Context context = request.getContext();   //獲取當前請求所屬的context
        if (context == null) {  //如果沒法找到context,那麼可以直接返回錯誤了
            response.sendError
                (HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                 sm.getString("standardHost.noContext"));
            return;
        }
 
        context.bind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
 
        if (request.isAsyncSupported()) {  //設定當前請求是否支援非同步
            request.setAsyncSupported(context.getPipeline().isAsyncSupported());
        }
 
        // Don't fire listeners during async processing
        // If a request init listener throws an exception, the request is
        // aborted
        boolean asyncAtStart = request.isAsync();
        // An async error page may dispatch to another resource. This flag helps
        // ensure an infinite error handling loop is not entered
        boolean errorAtStart = response.isError();
        if (asyncAtStart || context.fireRequestInitEvent(request)) {   //用於讓ServletRequestListener,表示有請求進來了
 
            // Ask this Context to process this request
            try {
                context.getPipeline().getFirst().invoke(request, response);   //呼叫所屬的context來處理了
            } catch (Throwable t) {   //如果有異常的話,那麼需要返回錯誤
                ExceptionUtils.handleThrowable(t);
                if (errorAtStart) {
                    container.getLogger().error("Exception Processing " +
                            request.getRequestURI(), t);
                } else {
                    request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, t);
                    throwable(request, response, t);
                }
            }
 
            // If the request was async at the start and an error occurred then
            // the async error handling will kick-in and that will fire the
            // request destroyed event *after* the error handling has taken
            // place
            if (!(request.isAsync() || (asyncAtStart &&
                    request.getAttribute(
                            RequestDispatcher.ERROR_EXCEPTION) != null))) {
                // Protect against NPEs if context was destroyed during a
                // long running request.
                if (context.getState().isAvailable()) {
                    if (!errorAtStart) {
                        // Error page processing
                        response.setSuspended(false);
 
                        Throwable t = (Throwable) request.getAttribute(
                                RequestDispatcher.ERROR_EXCEPTION);
 
                        if (t != null) {
                            throwable(request, response, t);
                        } else {
                            status(request, response);
                        }
                    }
 
                    context.fireRequestDestroyEvent(request);
                }
            }
        }
 
        // Access a session (if present) to update last accessed time, based on a
        // strict interpretation of the specification
        if (ACCESS_SESSION) {
            request.getSession(false);
        }
 
        context.unbind(Globals.IS_SECURITY_ENABLED, MY_CLASSLOADER);
    }

嗯,程式碼雖然挺長的,其實主要也就是要呼叫當前請求所屬的context的pipeline來處理這個請求的。。。

嗯,這裡其實StandardHost部分的內容也就差不多了吧。。

HostConfig

接下來來看看前面提到的HostConfig物件。。它將會用於監聽host物件的生命週期事件,例如啟動,停止是什麼的。。。

在這裡插入圖片描述

繼承體系還是蠻簡單的吧,實現了LifecycleListener介面,那麼表示當前物件可以響應lifecycle物件的生命週期事件,例如啟動停止。。。接下來來看看它的一些重要的屬性以及建構函式吧:

    protected String contextClass = "org.apache.catalina.core.StandardContext";   //用到的context的型別的名字
 
    protected Host host = null;   //監聽的host物件
 
    protected ObjectName oname = null;   //在jmx上面註冊的名字
 
    protected static final StringManager sm =
        StringManager.getManager(Constants.Package);
 
    protected boolean deployXML = false;   //是否要處理app的context的xml配置檔案
 
 
    protected boolean copyXML = false;   //是否要將xml配置檔案移動到/conf/enginename/hostname/下面
 
    protected boolean unpackWARs = false;   //是否要解壓war
 
    protected final Map<String, DeployedApplication> deployed =  //所有已經部署的應用,key是context的名字
            new ConcurrentHashMap<>();
    protected final ArrayList<String> serviced = new ArrayList<>();
 
    protected Digester digester = createDigester(contextClass);   //用於即系xml檔案的
    private final Object digesterLock = new Object();
 
    protected final Set<String> invalidWars = new HashSet<>();   //忽略的war包
 
    public String getContextClass() {  //獲取用到的context的型別
 
        return (this.contextClass);
 
    }

這裡屬性有些還是非常很總要的,例如deployed,用於代表每一個已經部署的web應用程式。。。接下來來看看它的lifecycleEvent方法的定義吧,也就是它是如何響應事件的:

  //相應監聽的host的生命週期的事件
    public void lifecycleEvent(LifecycleEvent event) {
 
        // Identify the host we are associated with
        try {
            host = (Host) event.getLifecycle();  //當前所監聽的lifecycle物件,這裡監聽的是host物件
            if (host instanceof StandardHost) {   //根據host的資訊,來設定一些配置
                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.START_EVENT)) {
            start();   //開始進行部署
        } else if (event.getType().equals(Lifecycle.STOP_EVENT)) {
            stop();   //停止
        }
    }

這裡首先是在host物件上面來拿一些配置的資訊,然後根據事件的型別進行相應的處理。。這裡就來看看對於啟動的事件是怎麼相應的吧:

    //當監聽的host啟動的時候會執行這個方法,其實主要是context的部署
    public void start() {
 
        if (log.isDebugEnabled())
            log.debug(sm.getString("hostConfig.start"));
 
        try {
            ObjectName hostON = host.getObjectName();  //獲取host的jmx上面註冊的名字
            oname = new ObjectName
                (hostON.getDomain() + ":type=Deployer,host=" + host.getName());  //根據host的名字生成當前物件的名字
            Registry.getRegistry(null, null).registerComponent
                (this, oname, this.getClass().getName());  //在jmx上面註冊當前物件
        } catch (Exception e) {
            log.error(sm.getString("hostConfig.jmx.register", oname), e);
        }
 
        if (host.getCreateDirs()) {  //如果要建立資料夾,這個是用於存放host的配置檔案。/conf/enginename/hostname,還有就是app的目錄
            File[] dirs = new File[] {host.getAppBaseFile(),host.getConfigBaseFile()};
            for (int i=0; i<dirs.length; i++) {
                if (!dirs[i].mkdirs() && !dirs[i].isDirectory()) {  //如果目錄不存在的話,那麼建立目錄
                    log.error(sm.getString("hostConfig.createDirs",dirs[i]));
                }
            }
        }
 
        if (!host.getAppBaseFile().isDirectory()) {  //如果app的目錄不是資料夾,那麼錯誤了
            log.error(sm.getString("hostConfig.appBase", host.getName(),
                    host.getAppBaseFile().getPath()));
            host.setDeployOnStartup(false);
            host.setAutoDeploy(false);
        }
 
        if (host.getDeployOnStartup())   //一般都是
            deployApps();  //開始部署應用
 
    }

這裡要做的其實主要是在jmx上的註冊,然後對配置檔案目錄的處理,接著再是呼叫deployApps方法來具體的部署應用web應用程式

    //app的部署
    protected void deployApps() {
 
        File appBase = host.getAppBaseFile();   //獲取app的路徑目錄的資料夾引用  /webapps
        File configBase = host.getConfigBaseFile();  //獲取host的配置檔案的路徑   /conf/enginename/hostname/
        String[] filteredAppPaths = filterAppPaths(appBase.list());   //這裡過濾一下app的路徑,用host裡的正規表示式來判斷資料夾的名字是否符合規定
        // Deploy XML descriptors from configBase
        deployDescriptors(configBase, configBase.list());   //先處理host的配置
        // Deploy WARs
        deployWARs(appBase, filteredAppPaths);   //部署app資料夾的war包
        // Deploy expanded folders
        deployDirectories(appBase, filteredAppPaths);   //部署app資料夾裡面的檔案
 
    }

首先獲取了host的配置檔案的目錄以及存放app的資料夾的目錄,然後對app的檔案的名字進行一些過濾,畢竟context的名字不能隨便取的嘛。。接著就是開始部署應用程式了,這裡分為兩種吧,一種是不是war包型別的,另外一種就是部署資料夾型別的。。。

這裡就來看看部署資料夾型別的吧,war包無非就是多了一層解壓而已:

    // 部署應用,資料夾型別的,前面是所有app所在的目錄的引用,後面是要部署的資料夾的名字
    protected void deployDirectories(File appBase, String[] files) {
    	
        if (files == null)
            return;
 
        ExecutorService es = host.getStartStopExecutor();  //後去host物件用於啟動停止子container的executor
        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 dir = new File(appBase, files[i]);  //建立檔案的file引用  
            if (dir.isDirectory()) {  //這裡需要是一個資料夾
                ContextName cn = new ContextName(files[i], false);   //根據資料夾的名字來設定context的名字
 
                if (isServiced(cn.getName()) || deploymentExists(cn.getName()))  //是否有同名的
                    continue;
                results.add(es.submit(new DeployDirectory(this, cn, dir)));  //新增一個deploy資料夾的任務,派遣到executor裡面進行 其實是config.deployDirectory
            }
        }
 
        for (Future<?> result : results) {
            try {
                result.get();
            } catch (Exception e) {
                log.error(sm.getString(
                        "hostConfig.deployDir.threaded.error"), e);
            }
        }
    }

這裡是遍歷當前app所在資料夾,然後根據資料夾的名字來建立context的名字,然後在host的executor上面提交部署的任務,具體的執行如下:

    //第一個引數是context的名字,第二個引數是資料夾的引用
    protected void deployDirectory(ContextName cn, File dir) {
 
 
        // Deploy the application in this directory
        if( log.isInfoEnabled() )  //列印正在部署啥
            log.info(sm.getString("hostConfig.deployDir",
                    dir.getAbsolutePath()));
 
        Context context = null;
        //有的web應用可能有定義context的配置
        File xml = new File(dir, Constants.ApplicationContextXml);    //"META-INF/context.xml";  當前context的配置檔案,這個也不一一定有
        File xmlCopy =
                new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");  //獲取host的配置資料夾裡面當前context的配置,這個不一定有
 
 
        DeployedApplication deployedApp;   //用於引用已經部署的app
        boolean copyThisXml = copyXML;
 
        try {
            if (deployXML && xml.exists()) {  //根據應用的配置來建立
                synchronized (digesterLock) {
                    try {
                        context = (Context) digester.parse(xml);
                    } catch (Exception e) {
                        log.error(sm.getString(
                                "hostConfig.deployDescriptor.error",
                                xml), e);
                        context = new FailedContext();
                    } finally {
                        if (context == null) {
                            context = new FailedContext();
                        }
                        digester.reset();
                    }
                }
 
                if (copyThisXml == false && context instanceof StandardContext) {
                    // Host is using default value. Context may override it.
                    copyThisXml = ((StandardContext) context).getCopyXML();
                }
 
                if (copyThisXml) {
                    InputStream is = null;
                    OutputStream os = null;
                    try {
                        is = new FileInputStream(xml);
                        os = new FileOutputStream(xmlCopy);
                        IOTools.flow(is, os);
                        // Don't catch IOE - let the outer try/catch handle it
                    } finally {
                        try {
                            if (is != null) is.close();
                        } catch (IOException e){
                            // Ignore
                        }
                        try {
                            if (os != null) os.close();
                        } catch (IOException e){
                            // Ignore
                        }
                    }
                    context.setConfigFile(xmlCopy.toURI().toURL());
                } else {
                    context.setConfigFile(xml.toURI().toURL());
                }
            } else if (!deployXML && xml.exists()) {
                // Block deployment as META-INF/context.xml may contain security
                // configuration necessary for a secure deployment.
                log.error(sm.getString("hostConfig.deployDescriptor.blocked",
                        cn.getPath(), xml, xmlCopy));
                context = new FailedContext();
            } else {  //一般沒有context的配置的話,就在這裡建立context   org.apache.catalina.core.StandardContext
                context = (Context) Class.forName(contextClass).newInstance();
            }
 
            Class<?> clazz = Class.forName(host.getConfigClass());  //為context建立config物件   org.apache.catalina.startup.ContextConfig
            LifecycleListener listener =
                (LifecycleListener) clazz.newInstance();    // 建立listener的物件,然後新增到context上面去
            context.addLifecycleListener(listener);  
 
            context.setName(cn.getName());  //設定當前context的名字
            context.setPath(cn.getPath());   //應用所在的路徑
            context.setWebappVersion(cn.getVersion());  //當前版本
            context.setDocBase(cn.getBaseName());
            host.addChild(context);   //在host新增context,在host裡面,會將context的name與context對應起來,而且這裡還會進行context的啟動
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
            log.error(sm.getString("hostConfig.deployDir.error",
                    dir.getAbsolutePath()), t);
        } finally {
            deployedApp = new DeployedApplication(cn.getName(),  //建立DeployedApplication物件,表設一個部署的應用
                    xml.exists() && deployXML && copyThisXml);
 
            // Fake re-deploy resource to detect if a WAR is added at a later
            // point
            //重新熱部署的東西
            deployedApp.redeployResources.put(dir.getAbsolutePath() + ".war",
                    Long.valueOf(0));  
            deployedApp.redeployResources.put(dir.getAbsolutePath(),
                    Long.valueOf(dir.lastModified()));
            if (deployXML && xml.exists()) {  //如果有context的配置檔案
                if (copyThisXml) {
                    deployedApp.redeployResources.put(
                            xmlCopy.getAbsolutePath(),
                            Long.valueOf(xmlCopy.lastModified()));
                } else {
                    deployedApp.redeployResources.put(
                            xml.getAbsolutePath(),
                            Long.valueOf(xml.lastModified()));
                    // Fake re-deploy resource to detect if a context.xml file is
                    // added at a later point
                    deployedApp.redeployResources.put(
                            xmlCopy.getAbsolutePath(),
                            Long.valueOf(0));
                }
            } else {
                // Fake re-deploy resource to detect if a context.xml file is
                // added at a later point
                deployedApp.redeployResources.put(
                        xmlCopy.getAbsolutePath(),
                        Long.valueOf(0));
                if (!xml.exists()) {
                    deployedApp.redeployResources.put(
                            xml.getAbsolutePath(),
                            Long.valueOf(0));
                }
            }
            addWatchedResources(deployedApp, dir.getAbsolutePath(), context);   //新增web應用程式資源的監控
            // Add the global redeploy resources (which are never deleted) at
            // the end so they don't interfere with the deletion process
            addGlobalRedeployResources(deployedApp);  //新增全域性的資源
        }
 
        deployed.put(cn.getName(), deployedApp);  //表示這個app已經部署了,key是當前context的名字,後面是
    }

程式碼也還算是比較長的吧,其實主要要做得到事情就是處理當前web應用程式的context的配置,然後建立context的物件,然後呼叫host物件的addChild方法將當前建立的context加入到host裡面去。。。

好啦,到這裡就算差不多了。。乾貨不多吧,主要就集中在context的建立和部署上了。。。



參考:
《Tomcat原始碼閱讀之StandardHost與HostConfig的分析》

相關文章