Tomcat7中web应用加载原理(二)web.xml解析

前一篇文章讲了org.apache.catalina.startup.HostConfig的lifecycleEvent方法中所做的事情。最后看到在Tomcat启动时或启动后(后台线程定时扫描)会调用HostConfig类的deployApps方法:

    /**
     * Deploy applications for any directories or WAR files that are found
     * in our "application root" directory.
     */
    protected void deployApps() {

        File appBase = appBase();
        File configBase = configBase();
        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);

    }

可以看到这里部署应用有三种方式:XML文件描述符、WAR包、文件目录。三种方式部署的总体流程很相似,都是一个web应用分配一个线程来处理,这里统一放到与Host内部的线程池对象中(startStopExecutor),所以有时会看到在默认配置下Tomcat启动后可能有一个叫“-startStop-”的线程还会运行一段时间才结束。但浏览这三种部署方式的实现代码,里面都是构建一个Context对象,并将构建好的Context对象与Host组件关联起来(即调用host.addChild(context)这句,具体代码在HostConfig类的deployDescriptor(ContextName cn, File contextXml)、deployDirectory(ContextName cn, File dir)、deployWAR(ContextName cn, File war)三个方法中,这里不再贴出代码来详细分析)。

前一篇文章只分析到这步,可以看出与一个web应用相对应的一个Context对象已经构建出来了,但如果容器只执行到这里根本无法响应一个浏览器的一次请求。就web服务器的实现来看一次请求过来除了需要根据内部Context构建找到这次请求访问的web应用具体所对应的Context对象,还需要包含web应用中具体的哪个Servlet来处理这次请求,中间是否还需要执行相应的过滤器(filter)、监听器(listener)等,做过java的web开发的都知道,这些信息是配置在一个web应用的WEB-INF\web.xml文件的(servlet3中已经支持将这些配置信息放到Java文件的注解中,但万变不离其宗,总归要在web应用的某个地方说明,并在容器启动时加载,这样才能真正提供web服务,响应请求)。

看到这里可以猜到Tomcat容器加载web应用时必定会有对于每个应用的web.xml文件的解析过程,本文就来看看这个解析过程。

在本文开头提到的三种部署应用的实现代码中有一些共通的代码,这里摘出来说明一下:

            Class<?> clazz = Class.forName(host.getConfigClass());
            LifecycleListener listener =
                (LifecycleListener) clazz.newInstance();
            context.addLifecycleListener(listener);
            host.addChild(context);

第一段是在所有Context对象构建时会添加一个监听器,这里监听器的类名是StandardHost类的实例变量configClass,其默认值就是org.apache.catalina.startup.ContextConfig。第二段是将当前构建的Context对象添加到父容器Host对象中。

先看下StandardHost的addChild方法的实现:

    public void addChild(Container child) {

        child.addLifecycleListener(new MemoryLeakTrackingListener());

        if (!(child instanceof Context))
            throw new IllegalArgumentException
                (sm.getString("standardHost.notContext"));
        super.addChild(child);

    }

可以看到这段代码最后调用了父类的addChild方法:

    public void addChild(Container child) {
        if (Globals.IS_SECURITY_ENABLED) {
            PrivilegedAction<Void> dp =
                new PrivilegedAddChild(child);
            AccessController.doPrivileged(dp);
        } else {
            addChildInternal(child);
        }
    }

这里看下addChildInternal方法的实现:

    private void addChildInternal(Container child) {

        if( log.isDebugEnabled() )
            log.debug("Add child " + child + " " + this);
        synchronized(children) {
            if (children.get(child.getName()) != null)
                throw new IllegalArgumentException("addChild:  Child name '" +
                                                   child.getName() +
                                                   "' is not unique");
            child.setParent(this);  // May throw IAE
            children.put(child.getName(), child);
        }

        // Start child
        // Don't do this inside sync block - start can be a slow process and
        // locking the children object can cause problems elsewhere
        if ((getState().isAvailable() ||
                LifecycleState.STARTING_PREP.equals(getState())) &&
                startChildren) {
            try {
                child.start();
            } catch (LifecycleException e) {
                log.error("ContainerBase.addChild: start: ", e);
                throw new IllegalStateException
                    ("ContainerBase.addChild: start: " + e);
            }
        }

        fireContainerEvent(ADD_CHILD_EVENT, child);
    }

可以看到会调用子容器的start方法,就是指调用StandardContext的start方法。

即给host对象添加子容器时将会调用子容器的start方法,按照前面文章的分析,调用StandardContext的start方法最终会调用org.apache.catalina.core.StandardContext类的startInternal方法(该方法代码较长,建议自己阅读,不再贴出),这里将会发布一系列事件,按调用前后顺序这些事件包括:BEFORE_INIT_EVENT、AFTER_INIT_EVENT、BEFORE_START_EVENT、CONFIGURE_START_EVENT、START_EVENT、AFTER_START_EVENT。

前面提到在构建Context对象时都会注册一个监听器org.apache.catalina.startup.ContextConfig,看下这个类的lifecycleEvent方法中(为什么会执行这个方法可以看本博文章的分析)监听了哪些事件:

    /**
     * Process events for an associated Context.
     *
     * @param event The lifecycle event that has occurred
     */
    @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的start方法调用相关的事件监听前后顺序为:AFTER_INIT_EVENT(执行init方法)、BEFORE_START_EVENT(执行beforeStart方法)、CONFIGURE_START_EVENT(执行configureStart方法)。

在configureStart方法将直接调用webConfig方法,正是在这个方法中将会解析web.xml文件:

    /**
     * Scan the web.xml files that apply to the web application and merge them
     * using the rules defined in the spec. For the global web.xml files,
     * where there is duplicate configuration, the most specific level wins. ie
     * an application's web.xml takes precedence over the host level or global
     * web.xml file.
     */
    protected void webConfig() {
        /*
         * Anything and everything can override the global and host defaults.
         * This is implemented in two parts
         * - Handle as a web fragment that gets added after everything else so
         *   everything else takes priority
         * - Mark Servlets as overridable so SCI configuration can replace
         *   configuration from the defaults
         */

        /*
         * The rules for annotation scanning are not as clear-cut as one might
         * think. Tomcat implements the following process:
         * - As per SRV.1.6.2, Tomcat will scan for annotations regardless of
         *   which Servlet spec version is declared in web.xml. The EG has
         *   confirmed this is the expected behaviour.
         * - As per http://java.net/jira/browse/SERVLET_SPEC-36, if the main
         *   web.xml is marked as metadata-complete, JARs are still processed
         *   for SCIs.
         * - If metadata-complete=true and an absolute ordering is specified,
         *   JARs excluded from the ordering are also excluded from the SCI
         *   processing.
         * - If an SCI has a @HandlesType annotation then all classes (except
         *   those in JARs excluded from an absolute ordering) need to be
         *   scanned to check if they match.
         */
        Set<WebXml> defaults = new HashSet<WebXml>();
        defaults.add(getDefaultWebXmlFragment());

        WebXml webXml = createWebXml();

        // Parse context level web.xml
        InputSource contextWebXml = getContextWebXmlSource();
        parseWebXml(contextWebXml, webXml, false);

        ServletContext sContext = context.getServletContext();

        // Ordering is important here

        // Step 1. Identify all the JARs packaged with the application
        // If the JARs have a web-fragment.xml it will be parsed at this
        // point.
        Map<String,WebXml> fragments = processJarsForWebFragments();

        // Step 2. Order the fragments.
        Set<WebXml> orderedFragments = null;
        orderedFragments =
                WebXml.orderWebFragments(webXml, fragments, sContext);

        // Step 3. Look for ServletContainerInitializer implementations
        if (ok) {
            processServletContainerInitializers(orderedFragments);
        }

        if  (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
            // Step 4. Process /WEB-INF/classes for annotations
            if (ok) {
                // Hack required by Eclipse's "serve modules without
                // publishing" feature since this backs WEB-INF/classes by
                // multiple locations rather than one.
                NamingEnumeration<Binding> listBindings = null;
                try {
                    try {
                        listBindings = context.getResources().listBindings(
                                "/WEB-INF/classes");
                    } catch (NameNotFoundException ignore) {
                        // Safe to ignore
                    }
                    while (listBindings != null &&
                            listBindings.hasMoreElements()) {
                        Binding binding = listBindings.nextElement();
                        if (binding.getObject() instanceof FileDirContext) {
                            File webInfClassDir = new File(
                                    ((FileDirContext) binding.getObject()).getDocBase());
                            processAnnotationsFile(webInfClassDir, webXml,
                                    webXml.isMetadataComplete());
                        } else {
                            String resource =
                                    "/WEB-INF/classes/" + binding.getName();
                            try {
                                URL url = sContext.getResource(resource);
                                processAnnotationsUrl(url, webXml,
                                        webXml.isMetadataComplete());
                            } catch (MalformedURLException e) {
                                log.error(sm.getString(
                                        "contextConfig.webinfClassesUrl",
                                        resource), e);
                            }
                        }
                    }
                } catch (NamingException e) {
                    log.error(sm.getString(
                            "contextConfig.webinfClassesUrl",
                            "/WEB-INF/classes"), e);
                }
            }

            // Step 5. Process JARs for annotations - only need to process
            // those fragments we are going to use
            if (ok) {
                processAnnotations(
                        orderedFragments, webXml.isMetadataComplete());
            }

            // Cache, if used, is no longer required so clear it
            javaClassCache.clear();
        }

        if (!webXml.isMetadataComplete()) {
            // Step 6. Merge web-fragment.xml files into the main web.xml
            // file.
            if (ok) {
                ok = webXml.merge(orderedFragments);
            }

            // Step 7. Apply global defaults
            // Have to merge defaults before JSP conversion since defaults
            // provide JSP servlet definition.
            webXml.merge(defaults);

            // Step 8. Convert explicitly mentioned jsps to servlets
            if (ok) {
                convertJsps(webXml);
            }

            // Step 9. Apply merged web.xml to Context
            if (ok) {
                webXml.configureContext(context);
            }
        } else {
            webXml.merge(defaults);
            convertJsps(webXml);
            webXml.configureContext(context);
        }

        // Step 9a. Make the merged web.xml available to other
        // components, specifically Jasper, to save those components
        // from having to re-generate it.
        // TODO Use a ServletContainerInitializer for Jasper
        String mergedWebXml = webXml.toXml();
        sContext.setAttribute(
               org.apache.tomcat.util.scan.Constants.MERGED_WEB_XML,
               mergedWebXml);
        if (context.getLogEffectiveWebXml()) {
            log.info("web.xml:\n" + mergedWebXml);
        }

        // Always need to look for static resources
        // Step 10. Look for static resources packaged in JARs
        if (ok) {
            // Spec does not define an order.
            // Use ordered JARs followed by remaining JARs
            Set<WebXml> resourceJars = new LinkedHashSet<WebXml>();
            if (orderedFragments != null) {
                for (WebXml fragment : orderedFragments) {
                    resourceJars.add(fragment);
                }
            }
            for (WebXml fragment : fragments.values()) {
                if (!resourceJars.contains(fragment)) {
                    resourceJars.add(fragment);
                }
            }
            processResourceJARs(resourceJars);
            // See also StandardContext.resourcesStart() for
            // WEB-INF/classes/META-INF/resources configuration
        }

        // Step 11. Apply the ServletContainerInitializer config to the
        // context
        if (ok) {
            for (Map.Entry<ServletContainerInitializer,
                    Set<Class<?>>> entry :
                        initializerClassMap.entrySet()) {
                if (entry.getValue().isEmpty()) {
                    context.addServletContainerInitializer(
                            entry.getKey(), null);
                } else {
                    context.addServletContainerInitializer(
                            entry.getKey(), entry.getValue());
                }
            }
        }
    }

这个方法里面做的事情,在英文注释中说的很清楚了,概括起来包括合并Tomcat全局web.xml、当前应用中的web.xml、web-fragment.xml和web应用的注解中的配置信息,并将解析出的各种配置信息(如servlet配置、filter配置等)关联到Context对象中(在上面的代码第140行:webXml.configureContext(context))。

看下configureContext方法:

    /**
     * Configure a {@link Context} using the stored web.xml representation.
     *
     * @param context   The context to be configured
     */
    public void configureContext(Context context) {
        // 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(publicId);

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

        for (Entry<String, String> entry : contextParams.entrySet()) {
            context.addParameter(entry.getKey(), entry.getValue());
        }
        context.setDisplayName(displayName);
        context.setDistributable(distributable);
        for (ContextLocalEjb ejbLocalRef : ejbLocalRefs.values()) {
            context.getNamingResources().addLocalEjb(ejbLocalRef);
        }
        for (ContextEjb ejbRef : ejbRefs.values()) {
            context.getNamingResources().addEjb(ejbRef);
        }
        for (ContextEnvironment environment : envEntries.values()) {
            context.getNamingResources().addEnvironment(environment);
        }
        for (ErrorPage errorPage : errorPages.values()) {
            context.addErrorPage(errorPage);
        }
        for (FilterDef filter : filters.values()) {
            if (filter.getAsyncSupported() == null) {
                filter.setAsyncSupported("false");
            }
            context.addFilterDef(filter);
        }
        for (FilterMap filterMap : filterMaps) {
            context.addFilterMap(filterMap);
        }
        for (JspPropertyGroup jspPropertyGroup : jspPropertyGroups) {
            JspPropertyGroupDescriptor descriptor =
                new ApplicationJspPropertyGroupDescriptor(jspPropertyGroup);
            context.getJspConfigDescriptor().getJspPropertyGroups().add(
                    descriptor);
        }
        for (String listener : listeners) {
            context.addApplicationListener(
                    new ApplicationListener(listener, false));
        }
        for (Entry<String, String> entry : localeEncodingMappings.entrySet()) {
            context.addLocaleEncodingMappingParameter(entry.getKey(),
                    entry.getValue());
        }
        // Prevents IAE
        if (loginConfig != null) {
            context.setLoginConfig(loginConfig);
        }
        for (MessageDestinationRef mdr : messageDestinationRefs.values()) {
            context.getNamingResources().addMessageDestinationRef(mdr);
        }

        // messageDestinations were ignored in Tomcat 6, so ignore here

        context.setIgnoreAnnotations(metadataComplete);
        for (Entry<String, String> entry : mimeMappings.entrySet()) {
            context.addMimeMapping(entry.getKey(), entry.getValue());
        }
        // Name is just used for ordering
        for (ContextResourceEnvRef resource : resourceEnvRefs.values()) {
            context.getNamingResources().addResourceEnvRef(resource);
        }
        for (ContextResource resource : resourceRefs.values()) {
            context.getNamingResources().addResource(resource);
        }
        for (SecurityConstraint constraint : securityConstraints) {
            context.addConstraint(constraint);
        }
        for (String role : securityRoles) {
            context.addSecurityRole(role);
        }
        for (ContextService service : serviceRefs.values()) {
            context.getNamingResources().addService(service);
        }
        for (ServletDef servlet : servlets.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 : servletMappings.entrySet()) {
            context.addServletMapping(entry.getKey(), entry.getValue());
        }
        if (sessionConfig != null) {
            if (sessionConfig.getSessionTimeout() != null) {
                context.setSessionTimeout(
                        sessionConfig.getSessionTimeout().intValue());
            }
            SessionCookieConfig scc =
                context.getServletContext().getSessionCookieConfig();
            scc.setName(sessionConfig.getCookieName());
            scc.setDomain(sessionConfig.getCookieDomain());
            scc.setPath(sessionConfig.getCookiePath());
            scc.setComment(sessionConfig.getCookieComment());
            if (sessionConfig.getCookieHttpOnly() != null) {
                scc.setHttpOnly(sessionConfig.getCookieHttpOnly().booleanValue());
            }
            if (sessionConfig.getCookieSecure() != null) {
                scc.setSecure(sessionConfig.getCookieSecure().booleanValue());
            }
            if (sessionConfig.getCookieMaxAge() != null) {
                scc.setMaxAge(sessionConfig.getCookieMaxAge().intValue());
            }
            if (sessionConfig.getSessionTrackingModes().size() > 0) {
                context.getServletContext().setSessionTrackingModes(
                        sessionConfig.getSessionTrackingModes());
            }
        }
        for (Entry<String, String> entry : taglibs.entrySet()) {
            TaglibDescriptor descriptor = new ApplicationTaglibDescriptor(
                    entry.getValue(), entry.getKey());
            context.getJspConfigDescriptor().getTaglibs().add(descriptor);
        }

        // Context doesn't use version directly

        for (String welcomeFile : welcomeFiles) {
            /*
             * The following will result in a welcome file of "" so don't add
             * that to the context
             * <welcome-file-list>
             *   <welcome-file/>
             * </welcome-file-list>
             */
            if (welcomeFile != null && welcomeFile.length() > 0) {
                context.addWelcomeFile(welcomeFile);
            }
        }

        // Do this last as it depends on servlets
        for (JspPropertyGroup jspPropertyGroup : jspPropertyGroups) {
            String jspServletName = context.findServletMapping("*.jsp");
            if (jspServletName == null) {
                jspServletName = "jsp";
            }
            if (context.findChild(jspServletName) != null) {
                for (String urlPattern : jspPropertyGroup.getUrlPatterns()) {
                    context.addServletMapping(urlPattern, jspServletName, true);
                }
            } else {
                if(log.isDebugEnabled()) {
                    for (String urlPattern : jspPropertyGroup.getUrlPatterns()) {
                        log.debug("Skiping " + urlPattern + " , no servlet " +
                                jspServletName);
                    }
                }
            }
        }

        for (Entry<String, String> entry : postConstructMethods.entrySet()) {
            context.addPostConstructMethod(entry.getKey(), entry.getValue());
        }

        for (Entry<String, String> entry : preDestroyMethods.entrySet()) {
            context.addPreDestroyMethod(entry.getKey(), entry.getValue());
        }
    }

可以看到里面对context调用了各种set、add方法,从而将web.xml中的各种配置信息与表示一个web应用的context对象关联起来。

猜你喜欢

转载自tyrion.iteye.com/blog/1944144
今日推荐