Tomcat启动分析(十) - Context组件

Context组件表示一个Web应用,运行于一个特定的虚拟主机Host中。每个Web应用可以基于WAR文件,也可以基于相应的未打包目录。Catalina根据HTTP请求URI基于最长匹配选择处理该请求的Web应用,一旦选择,该Context就会根据定义好的servlet映射选择一个适当的servlet去处理。

Context组件

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

public class StandardContext extends ContainerBase implements Context, NotificationEmitter {
    private static final Log log = LogFactory.getLog(StandardContext.class);
    // ----------------------------------------------------------- Constructors
    /**
     * Create a new StandardContext component with the default basic Valve.
     */
    public StandardContext() {
        super();
        pipeline.setBasic(new StandardContextValve());
        broadcaster = new NotificationBroadcasterSupport();
        // Set defaults
        if (!Globals.STRICT_SERVLET_COMPLIANCE) {
            // Strict servlet compliance requires all extension mapped servlets
            // to be checked against welcome files
            resourceOnlyServlets.add("jsp");
        }
    }
    // ----------------------------------------------------- Instance Variables
    /**
     * Allow multipart/form-data requests to be parsed even when the
     * target servlet doesn't specify @MultipartConfig or have a
     * <multipart-config> element.
     */
    protected boolean allowCasualMultipartParsing = false;

    private boolean swallowAbortedUploads = true;
    private Map<ServletContainerInitializer,Set<Class<?>>> initializers =
        new LinkedHashMap<>();
    private URL configFile = null;
    private boolean configured = false;
    protected ApplicationContext context = null;
    private String encodedPath = null;
    private String path = null;
    private String docBase = null;
    private boolean reloadable = false;
    private boolean unpackWAR = true;
    private boolean copyXML = false;
    private boolean override = false;
    private boolean swallowOutput = false;

    // 省略一些代码

    private boolean validateClientProvidedNewSessionId = true;
    private boolean mapperContextRootRedirectEnabled = true;
    private boolean mapperDirectoryRedirectEnabled = false;
    private boolean useRelativeRedirects = !Globals.STRICT_SERVLET_COMPLIANCE;
    private boolean dispatchersUseEncodedPaths = true;
    private String requestEncoding = null;
    private String responseEncoding = null;
    private boolean allowMultipleLeadingForwardSlashInPath = false;
}
  • 成员变量的含义可以参考Context的配置文档
  • Context组件的构造函数为自己的Pipeline添加了基本阀StandardContextValve,addChild方法只能添加Wrapper组件。

组件初始化

StandardContext类的initInternal方法如下:

@Override
protected void initInternal() throws LifecycleException {
    super.initInternal();

    // Register the naming resources
    if (namingResources != null) {
        namingResources.init();
    }

    // Send j2ee.object.created notification
    if (this.getObjectName() != null) {
        Notification notification = new Notification("j2ee.object.created",
                this.getObjectName(), sequenceNumber.getAndIncrement());
        broadcaster.sendNotification(notification);
    }
}
  • 调用基类ContainerBase的对应方法为自己创建线程池;
  • 命名服务初始化;
  • 发送JMX的j2ee.object.created通知。

组件启动

由于StandardContext类的startInternal方法代码较多,故此处省略。简而言之,Context组件启动时按顺序做了如下工作:

  • 为该Context创建工作目录,如${catalina.base}/work/Engine名称/Host名称/Context的基本名称;
  • 创建WebResourceRoot用于读取Web应用的资源;
  • 创建Loader,Loader是ClassLoader的一种实现,可以按需重新加载;
  • 初始化字符集映射;
  • 启动Loader;
  • 发布事件Lifecycle.CONFIGURE_START_EVENT;
  • 启动子容器组件(即Wrapper组件);
  • 执行各ServletContainerInitializer回调方法;
  • 配置并初始化过滤器;
  • 加载被标识为“启动加载”的servlet;
  • 发布事件Lifecycle.START_EVENT。

ContextConfig监听器

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

成员变量

ContextConfig监听器重要的部分成员变量如下:

protected Context context = null;
protected String defaultWebXml = null;
protected boolean ok = false;
protected String originalDocBase = null;
protected final Map<ServletContainerInitializer, Set<Class<?>>> initializerClassMap =
        new LinkedHashMap<>();
protected final Map<Class<?>, Set<ServletContainerInitializer>> typeInitializerMap =
        new HashMap<>();
  • context变量引用与之关联的Context组件;
  • defaultWebXml变量表示默认的web.xml路径,若没有指定则是conf/web.xml;
  • ok变量表示配置和启动阶段是否正常;
  • originalDocBase变量表示Context的docBase;
  • initializerClassMap和typeInitializerMap变量都与ServletContainerInitializer有关。

响应生命周期事件

ContextConfig类实现的lifecycleEvent方法如下:

@Override
public void lifecycleEvent(LifecycleEvent event) {

    // Identify the context we are associated with
    try {
        context = (Context) event.getLifecycle();
    } catch (ClassCastException e) {
        log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
        return;
    }

    // Process the event that has occurred
    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
        configureStart();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
        beforeStart();
    } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
        // Restore docBase for management tools
        if (originalDocBase != null) {
            context.setDocBase(originalDocBase);
        }
    } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
        configureStop();
    } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
        init();
    } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
        destroy();
    }
}
  • 首先将发布事件的Context组件保存到context变量;
  • 然后针对不同事件执行不同的事件处理方法,比较重要的是Lifecycle.AFTER_INIT_EVENT和Lifecycle.CONFIGURE_START_EVENT两个事件。

初始化后事件

回忆LifecycleBase类的init方法,在调用initInternal方法后会调用setStateInternal更改组件的状态并发布LifecycleState.INITIALIZED枚举对应的事件,即初始化后事件Lifecycle.AFTER_INIT_EVENT。由于容器组件均继承自LifecycleBase类,因此StandardContext在执行initInternal方法后也会发布该事件,ContextConfig监听器对此事件的响应是执行init方法,该方法代码如下:

protected void init() {
    // Called from StandardContext.init()

    Digester contextDigester = createContextDigester();
    contextDigester.getParser();

    if (log.isDebugEnabled()) {
        log.debug(sm.getString("contextConfig.init"));
    }
    context.setConfigured(false);
    ok = true;

    contextConfig(contextDigester);
}
  • 首先创建了Digester对象;
  • contextConfig方法利用上述Digester对象解析<Context>配置,参见Context配置文档的Defining a context一节。

配置开始事件

StandardContext在启动时会发布配置开始事件Lifecycle.CONFIGURE_START_EVENT,ContextConfig监听器对此事件的响应是执行configureStart方法,该方法代码如下:

protected synchronized void configureStart() {
    // Called from StandardContext.start()

    if (log.isDebugEnabled()) {
        log.debug(sm.getString("contextConfig.start"));
    }

    if (log.isDebugEnabled()) {
        log.debug(sm.getString("contextConfig.xmlSettings",
                context.getName(),
                Boolean.valueOf(context.getXmlValidation()),
                Boolean.valueOf(context.getXmlNamespaceAware())));
    }

    webConfig();

    if (!context.getIgnoreAnnotations()) {
        applicationAnnotationsConfig();
    }
    if (ok) {
        validateSecurityRoles();
    }

    // Configure an authenticator if we need one
    if (ok) {
        authenticatorConfig();
    }

    // Dump the contents of this pipeline if requested
    if (log.isDebugEnabled()) {
        log.debug("Pipeline Configuration:");
        Pipeline pipeline = context.getPipeline();
        Valve valves[] = null;
        if (pipeline != null) {
            valves = pipeline.getValves();
        }
        if (valves != null) {
            for (int i = 0; i < valves.length; i++) {
                log.debug("  " + valves[i].getClass().getName());
            }
        }
        log.debug("======================");
    }

    // Make our application available if no problems were encountered
    if (ok) {
        context.setConfigured(true);
    } else {
        log.error(sm.getString("contextConfig.unavailable"));
        context.setConfigured(false);
    }
}

webConfig方法根据Servlet规范解析web.xml:

  • 先找出Web应用jar内的web-fragment.xml并把它们排序;
  • 查找ServletContainerInitializer接口的实现类;
  • 处理/WEB-INF/classes下Web资源内的注解,如@HandlesTypes、@WebServlet、@WebFilter和@WebListener等;
  • 处理jar内的注解,如@HandlesTypes、@WebServlet、@WebFilter和@WebListener等;
  • 将web-fragment.xml和默认web-fragment.xml合并到web.xml;
  • configureContext方法利用合并后的web.xml配置Context组件。

configureContext方法部分代码如下:

private void configureContext(WebXml webxml) {
    // As far as possible, process in alphabetical order so it is easy to
    // check everything is present
    // Some validation depends on correct public ID
    context.setPublicId(webxml.getPublicId());

    // Everything else in order
    context.setEffectiveMajorVersion(webxml.getMajorVersion());
    context.setEffectiveMinorVersion(webxml.getMinorVersion());

    for (Entry<String, String> entry : webxml.getContextParams().entrySet()) {
        context.addParameter(entry.getKey(), entry.getValue());
    }
    // 省略一些代码
    for (FilterDef filter : webxml.getFilters().values()) {
        if (filter.getAsyncSupported() == null) {
            filter.setAsyncSupported("false");
        }
        context.addFilterDef(filter);
    }
    for (FilterMap filterMap : webxml.getFilterMappings()) {
        context.addFilterMap(filterMap);
    }
    
    // 省略一些代码
    for (ServletDef servlet : webxml.getServlets().values()) {
        Wrapper wrapper = context.createWrapper();
        // Description is ignored
        // Display name is ignored
        // Icons are ignored

        // jsp-file gets passed to the JSP Servlet as an init-param

        if (servlet.getLoadOnStartup() != null) {
            wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
        }
        if (servlet.getEnabled() != null) {
            wrapper.setEnabled(servlet.getEnabled().booleanValue());
        }
        wrapper.setName(servlet.getServletName());
        Map<String,String> params = servlet.getParameterMap();
        for (Entry<String, String> entry : params.entrySet()) {
            wrapper.addInitParameter(entry.getKey(), entry.getValue());
        }
        wrapper.setRunAs(servlet.getRunAs());
        Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
        for (SecurityRoleRef roleRef : roleRefs) {
            wrapper.addSecurityReference(
                    roleRef.getName(), roleRef.getLink());
        }
        wrapper.setServletClass(servlet.getServletClass());
        MultipartDef multipartdef = servlet.getMultipartDef();
        if (multipartdef != null) {
            if (multipartdef.getMaxFileSize() != null &&
                    multipartdef.getMaxRequestSize()!= null &&
                    multipartdef.getFileSizeThreshold() != null) {
                wrapper.setMultipartConfigElement(new MultipartConfigElement(
                        multipartdef.getLocation(),
                        Long.parseLong(multipartdef.getMaxFileSize()),
                        Long.parseLong(multipartdef.getMaxRequestSize()),
                        Integer.parseInt(
                                multipartdef.getFileSizeThreshold())));
            } else {
                wrapper.setMultipartConfigElement(new MultipartConfigElement(
                        multipartdef.getLocation()));
            }
        }
        if (servlet.getAsyncSupported() != null) {
            wrapper.setAsyncSupported(
                    servlet.getAsyncSupported().booleanValue());
        }
        wrapper.setOverridable(servlet.isOverridable());
        context.addChild(wrapper);
    }
    for (Entry<String, String> entry :
            webxml.getServletMappings().entrySet()) {
        context.addServletMappingDecoded(entry.getKey(), entry.getValue());
    }
    // 省略一些代码
}

该方法做了很多工作,例如:

  • 将过滤器定义和映射添加到Context组件;
  • 对每个servlet定义,调用Context组件的createWrapper方法将servlet定义和参数包装成Wrapper,然后将其添加到Context组件中;
  • 将servlet映射添加到Context组件;
  • ...

参考文献

http://cmsblogs.com/?p=2672

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

推荐阅读更多精彩内容