zoukankan      html  css  js  c++  java
  • 【Spring】浅谈ContextLoaderListener及其上下文与DispatcherServlet的区别

    一般在使用SpingMVC开发的项目中,一般都会在web.xml文件中配置ContextLoaderListener监听器,如下:

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    在开始讲解这个之前先讲讲web工程的上下文,对于一个web容器,web容器提供了一个全局的上下文环境,这个上下文就是ServletContext,其为后面Spring IOC容器提供宿主环境。

    在web容器启动时会触发容器初始化事件,contextLoaderListener监听到这个事件后其contextInitialized方法就会被调用,在这个方法中,spring会初始化一个启动上下文,这个上下文就是根上下文,也就是WebApplicationContext,实际实现类一般是XmlWebApplicationContext,这个其实就是spring的IoC容器,这个IoC容器初始化完后,Spring会将它存储到ServletContext,可供后面获取到该IOC容器中的bean。

    下面一步步来跟进,看下ContextLoaderListener源码:

    public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    
        public ContextLoaderListener() {
        }
    
        public ContextLoaderListener(WebApplicationContext context) {
            super(context);
        }
    
        @Override
        public void contextInitialized(ServletContextEvent event) {
            initWebApplicationContext(event.getServletContext());
        }
    
        @Override
        public void contextDestroyed(ServletContextEvent event) {
            closeWebApplicationContext(event.getServletContext());
            ContextCleanupListener.cleanupAttributes(event.getServletContext());
        }
    }

    从上面可以看出ContextLoaderListener继承ContextLoader类并实现了ServletContextListener接口,ServletContextListener接口中只有初始化和销毁的两个方法,如下:

    public interface ServletContextListener extends EventListener {
        /**
         ** Notification that the web application initialization
         ** process is starting.
         ** All ServletContextListeners are notified of context
         ** initialization before any filter or servlet in the web
         ** application is initialized.
         */
        public void contextInitialized ( ServletContextEvent sce );
    
        /**
         ** Notification that the servlet context is about to be shut down.
         ** All servlets and filters have been destroy()ed before any
         ** ServletContextListeners are notified of context
         ** destruction.
         */
        public void contextDestroyed ( ServletContextEvent sce );
    }

    ContextLoaderListener主要的功能还是在继承的ContextLoader类中实现,接下来看看ContextLoaderListener中上下文初始化的方法,也就是:

    /**
     * Initialize the root web application context.
     */
    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }

    跟进initWebApplicationContext()方法,其调用的实现就在ContextLoader类中:

    /**
     * Initialize Spring's web application context for the given servlet context,
     * using the application context provided at construction time, or creating a new one
     * according to the "{@link #CONTEXT_CLASS_PARAM contextClass}" and
     * "{@link #CONFIG_LOCATION_PARAM contextConfigLocation}" context-params.
     * @param servletContext current servlet context
     * @return the new WebApplicationContext
     * @see #ContextLoader(WebApplicationContext)
     * @see #CONTEXT_CLASS_PARAM
     * @see #CONFIG_LOCATION_PARAM
     */
    public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        // 先判断ServletContext中是否已存在上下文,有的话说明已加载或配置信息有误(看下面抛出的异常信息)
        if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
            throw new IllegalStateException(
                    "Cannot initialize context because there is already a root application context present - " +
                    "check whether you have multiple ContextLoader* definitions in your web.xml!");
        }
    
        Log logger = LogFactory.getLog(ContextLoader.class);
        servletContext.log("Initializing Spring root WebApplicationContext");
        if (logger.isInfoEnabled()) {
            logger.info("Root WebApplicationContext: initialization started");
        }
        long startTime = System.currentTimeMillis();
    
        try {
            // Store context in local instance variable, to guarantee that
            // it is available on ServletContext shutdown.
            if (this.context == null) {
                // 创建WebApplicationContext上下文
                this.context = createWebApplicationContext(servletContext);
            }
            if (this.context instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
                if (!cwac.isActive()) {
                    // The context has not yet been refreshed -> provide services such as
                    // setting the parent context, setting the application context id, etc
                    if (cwac.getParent() == null) {
                        // The context instance was injected without an explicit parent ->
                        // determine parent for root web application context, if any.
                        // 加载父上下文
                        ApplicationContext parent = loadParentContext(servletContext);
                        cwac.setParent(parent);
                    }
                    // 对WebApplicationContext进行初始化,初始化参数从web.xml中取
                    configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
    
            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            }
            else if (ccl != null) {
                currentContextPerThread.put(ccl, this.context);
            }
    
            /* 省略部分代码 */
    }

    上面initWebApplicationContext()方法中,通过createWebApplicationContext(servletContext)创建root上下文(即IOC容器),之后Spring会以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE属性为Key,将该root上下文存储到ServletContext中,下面看看createWebApplicationContext(servletContext)源码:

    /**
     * Instantiate the root WebApplicationContext for this loader, either the
     * default context class or a custom context class if specified.
     * <p>This implementation expects custom contexts to implement the
     * {@link ConfigurableWebApplicationContext} interface.
     * Can be overridden in subclasses.
     * <p>In addition, {@link #customizeContext} gets called prior to refreshing the
     * context, allowing subclasses to perform custom modifications to the context.
     * @param sc current servlet context
     * @return the root WebApplicationContext
     * @see ConfigurableWebApplicationContext
     */
    protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
        // 确定载入的上下文的类型,参数是在web.xml中配置的contextClass(没有则使用默认的)
        Class<?> contextClass = determineContextClass(sc);
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
        }
        // 初始化WebApplicationContext并强转为ConfigurableWebApplicationContext类型
        return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
    }

    从上面源码也可以看出使用createWebApplicationContext方法创建的上下文肯定是实现了ConfigurableWebApplicationContext接口,否则抛出异常。上面createWebApplicationContext(servletContext)方法里的determineContextClass方法用于查找root上下文的Class类型,看源码:

    /**
     * Return the WebApplicationContext implementation class to use, either the
     * default XmlWebApplicationContext or a custom context class if specified.
     * @param servletContext current servlet context
     * @return the WebApplicationContext implementation class to use
     * @see #CONTEXT_CLASS_PARAM
     * @see org.springframework.web.context.support.XmlWebApplicationContext
     */
    protected Class<?> determineContextClass(ServletContext servletContext) {
        String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
        if (contextClassName != null) {
            try {
                return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
            }
            catch (ClassNotFoundException ex) {
                throw new ApplicationContextException(
                        "Failed to load custom context class [" + contextClassName + "]", ex);
            }
        }
        else {
            contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
            try {
                return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
            }
            catch (ClassNotFoundException ex) {
                throw new ApplicationContextException(
                        "Failed to load default context class [" + contextClassName + "]", ex);
            }
        }
    }

    从以上可以看到如果web.xml中配置了实现ConfigurableWebApplicationContext的contextClass类型就用那个参数,否则使用默认的XmlWebApplicationContext。

    上面ContextLoader类的initWebApplicationContext()方法里还有个加载父上下文的方法loadParentContext(ServletContext servletContext),也来看看其源码:

    /**
     * Template method with default implementation (which may be overridden by a
     * subclass), to load or obtain an ApplicationContext instance which will be
     * used as the parent context of the root WebApplicationContext. If the
     * return value from the method is null, no parent context is set.
     * <p>The main reason to load a parent context here is to allow multiple root
     * web application contexts to all be children of a shared EAR context, or
     * alternately to also share the same parent context that is visible to
     * EJBs. For pure web applications, there is usually no need to worry about
     * having a parent context to the root web application context.
     * <p>The default implementation uses
     * {@link org.springframework.context.access.ContextSingletonBeanFactoryLocator},
     * configured via {@link #LOCATOR_FACTORY_SELECTOR_PARAM} and
     * {@link #LOCATOR_FACTORY_KEY_PARAM}, to load a parent context
     * which will be shared by all other users of ContextsingletonBeanFactoryLocator
     * which also use the same configuration parameters.
     * @param servletContext current servlet context
     * @return the parent application context, or {@code null} if none
     * @see org.springframework.context.access.ContextSingletonBeanFactoryLocator
     */
    protected ApplicationContext loadParentContext(ServletContext servletContext) {
        ApplicationContext parentContext = null;
        String locatorFactorySelector = servletContext.getInitParameter(LOCATOR_FACTORY_SELECTOR_PARAM);
        String parentContextKey = servletContext.getInitParameter(LOCATOR_FACTORY_KEY_PARAM);
    
        if (parentContextKey != null) {
            // locatorFactorySelector may be null, indicating the default "classpath*:beanRefContext.xml"
            BeanFactoryLocator locator = ContextSingletonBeanFactoryLocator.getInstance(locatorFactorySelector);
            Log logger = LogFactory.getLog(ContextLoader.class);
            if (logger.isDebugEnabled()) {
                logger.debug("Getting parent context definition: using parent context key of '" +
                        parentContextKey + "' with BeanFactoryLocator");
            }
            this.parentContextRef = locator.useBeanFactory(parentContextKey);
            parentContext = (ApplicationContext) this.parentContextRef.getFactory();
        }
    
        return parentContext;
    }

    上面源码就是实现根据locatorFactorySelector和parentContextKey来给上下文设置父上下文,前提是我们在web.xml中配置了这两个参数,不过一般开发中很少会设置这两个参数,从上面源码的大段注释也可以看出如果没有的话父上下文就为空。

    在contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这个servlet可以配置多个,以DispatcherServlet为例,这个servlet实际上是一个标准的前端控制器,用以转发、处理每个servlet请求。DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文。有了这个parent上下文之后,再初始化自己持有的上下文。这个DispatcherServlet初始化自己上下文的工作在其initStrategies方法中实现的,基本工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是XmlWebApplicationContext。初始化完毕后,spring以与servlet的名字相关的属性为Key,也将其存到ServletContext中。这样每个servlet就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文(WebApplicationContext)。

    最后讲讲ContextLoaderListener与DispatcherServlet所创建的上下文ApplicationContext的区别:

    1. ContextLoaderListener中创建ApplicationContext主要用于整个Web应用程序需要共享的一些组件,比如DAO,数据库的ConnectionFactory等。而由DispatcherServlet创建的ApplicationContext主要用于和该Servlet相关的一些组件,比如Controller、ViewResovler等。
    2. 对于作用范围而言,在DispatcherServlet中可以引用由ContextLoaderListener所创建的ApplicationContext,而反过来不行。

    这两个ApplicationContext都是通过ServletContext的setAttribute方法放到ServletContext中的。从web.xml的配置可知ContextLoaderListener会先于DispatcherServlet创建ApplicationContext,DispatcherServlet在创建ApplicationContext时会先找到由ContextLoaderListener所创建的ApplicationContext,再将后者的ApplicationContext作为参数传给DispatcherServlet的ApplicationContext的setParent()方法,作为它的父上下文,在Spring源代可以看出:

    protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
        Class<?> contextClass = getContextClass();
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Servlet with name '" + getServletName() +
                    "' will try to create custom WebApplicationContext context of class '" +
                    contextClass.getName() + "'" + ", using parent context [" + parent + "]");
        }
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException(
                    "Fatal initialization error in servlet with name '" + getServletName() +
                    "': custom WebApplicationContext class [" + contextClass.getName() +
                    "] is not of type ConfigurableWebApplicationContext");
        }
        ConfigurableWebApplicationContext wac =
                (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
    
        wac.setEnvironment(getEnvironment());
    
        // 设置父ApplicationContext
        wac.setParent(parent);
        wac.setConfigLocation(getContextConfigLocation());
    
        configureAndRefreshWebApplicationContext(wac);
    
        return wac;
    }

    这里wac即为由DisptcherServlet创建的ApplicationContext,而parent则为有ContextLoaderListener创建的ApplicationContext。

    当Spring在执行ApplicationContext的getBean时,如果在自己context中找不到对应的bean,则会在父ApplicationContext中去找。这也解释了为什么我们可以在DispatcherServlet中获取到由ContextLoaderListener对应的ApplicationContext中的bean。

    父子两个WebApplicationContext带来的麻烦

    父WebApplicationContext里的bean可以在不同的子WebApplicationContext里共享,而不同的子WebApplicationContext里的bean区不干扰。
    但是实际上有会不少的问题:
    如果开发者不知道Spring mvc里分有两个WebApplicationContext,导致各种重复构造bean,各种bean无法注入的问题。
    有一些bean,比如全局的aop处理的类,如果先父WebApplicationContext里初始化了,那么子WebApplicationContext里的初始化的bean就没有处理到。如果在子WebApplicationContext里初始化,在父WebApplicationContext里的类就没有办法注入了。

  • 相关阅读:
    计算机 ------- 网络基础
    设计模式------单例模式
    svn 客户端查看用户名和密码
    Docker:DockerFile(DockerFile解析)
    Docker:容器数据卷(命令方式、DockerFile方式)
    SpringBoot再总结
    Docker:镜像(镜像原理)
    Docker:Docker命令(帮助命令、镜像命令、容器命令)
    Docker(Docker的概念与安装)
    ZooKeeper:zookeeper的常用命令(节点的新增、更新、删除)
  • 原文地址:https://www.cnblogs.com/weknow619/p/6341395.html
Copyright © 2011-2022 走看看