Tomcat源码阅读之StandardHost与HostConfig的分析

前面的文章分析了StandardEngine,这里来分析一下由Engine对象来管理的另外一种对象吧:Host。。

一般情况下,在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接口是如何定义的吧:

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是怎么实现的吧,先来看看简单的继承体系:


这个应该算是很简单的吧,首先也是一个容器。。。然后实现了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对象。。它将会用于监听host对象的生命周期事件,例如启动,停止是什么的。。。

Tomcat源码阅读之StandardHost与HostConfig的分析_第1张图片

继承体系还是蛮简单的吧,实现了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的分析)