zoukankan      html  css  js  c++  java
  • ContextLoaderListener解析

    推荐:spring源码

    每一个整合spring框架的项目中,总是不可避免地要在web.xml中加入这样一段配置。

    <!-- Spring配置文件开始  -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
            classpath:spring-config.xml
        </param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- Spring配置文件结束 -->

    而这段配置有什么作用,或者说ContextLoaderListener到底有什么作用。表示疑惑,我们研究一下ContextLoaderListener源码。

    public class ContextLoaderListener extends ContextLoader implements ServletContextListener

    ContextLoaderListener继承自ContextLoader,实现的是ServletContextListener接口。

    继承ContextLoader有什么作用? 
    ContextLoaderListener可以指定在Web应用程序启动时载入Ioc容器,正是通过ContextLoader来实现的,ContextLoader来完成实际的WebApplicationContext,也就是Ioc容器的初始化工作。

    实现ServletContextListener又有什么作用? 
    ServletContextListener接口里的函数会结合Web容器的生命周期被调用。因为ServletContextListener是ServletContext的监听者,如果ServletContext发生变化,会触发相应的事件,而监听器一直对事件监听,如果接收到了变化,就会做出预先设计好的相应动作。由于ServletContext变化而触发的监听器的响应具体包括:在服务器启动时,ServletContext被创建的时候,服务器关闭时,ServletContext将被销毁的时候等。

    那么ContextLoaderListener的作用是什么? 
    ContextLoaderListener的作用就是启动Web容器时,读取在contextConfigLocation中定义的xml文件,自动装配ApplicationContext的配置信息,并产生WebApplicationContext对象,然后将这个对象放置在ServletContext的属性里,这样我们只要得到Servlet就可以得到WebApplicationContext对象,并利用这个对象访问spring容器管理的bean。 
    简单来说,就是上面这段配置为项目提供了spring支持,初始化了Ioc容器。

    那又是怎么为我们的项目提供spring支持的呢? 
    上面说到“监听器一直对事件监听,如果接收到了变化,就会做出预先设计好的相应动作”。而监听器的响应动作就是在服务器启动时contextInitialized会被调用,关闭的时候contextDestroyed被调用。这里我们关注的是WebApplicationContext如何完成创建。因此销毁方法就暂不讨论。

        @Override
        public void contextInitialized(ServletContextEvent event) {
            //初始化webApplicationCotext
            initWebApplicationContext(event.getServletContext());
        }

    值得一提的是在initWebApplicationContext方法上面的注释提到(请对照原注释),WebApplicationContext根据在context-params中配置contextClass和contextConfigLocation完成初始化。有大概的了解后,接下来继续研究源码。

    public WebApplicationContext initWebApplicationContext(
                ServletContext servletContext) {
    
            // application对象中存放了spring context,则抛出异常
            // 其中ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = WebApplicationContext.class.getName() + ".ROOT";
            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!");
            }
    
            // 创建得到WebApplicationContext
            // createWebApplicationContext最后返回值被强制转换为ConfigurableWebApplicationContext类型
            if (this.context == null) {
                this.context = createWebApplicationContext(servletContext);
            }
    
            // 只要上一步强转成功,进入此方法(事实上走的就是这条路)
            if (this.context instanceof ConfigurableWebApplicationContext) {
    
                // 强制转换为ConfigurableWebApplicationContext类型
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
    
                // cwac尚未被激活,目前还没有进行配置文件加载
                if (!cwac.isActive()) {
    
                    // 加载配置文件
                    configureAndRefreshWebApplicationContext(cwac, servletContext);
    
                    【点击进入该方法发现这样一段:
    
                        //为wac绑定servletContext
                        wac.setServletContext(sc);
    
                        //CONFIG_LOCATION_PARAM=contextConfigLocation
                        //getInitParameter(CONFIG_LOCATION_PARAM)解释了为什么配置文件中需要有contextConfigLocation项
                        //需要注意还有sevletConfig.getInitParameter和servletContext.getInitParameter作用范围是不一样的
                        String initParameter = sc.getInitParameter(CONFIG_LOCATION_PARAM);
                        if (initParameter != null) {
                            //装配ApplicationContext的配置信息
                            wac.setConfigLocation(initParameter);
                        }
                    】
                }
            }
    
            // 把创建好的spring context,交给application内置对象,提供给监听器/过滤器/拦截器使用
            servletContext.setAttribute(
                    WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
                    this.context);
    
            // 返回webApplicationContext
            return this.context;
        }

    initWebApplicationContext中加载了contextConfigLocation的配置信息,初始化Ioc容器,说明了上述配置的必要性。而我有了新的疑问。

    WebApplicationContext和ServletContext是一种什么样的关系呢? 
    翻到源码,发现在ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE上面有:

    org.springframework.web.context.support.WebApplicationContextUtils#getWebApplicationContext
    
      org.springframework.web.context.support.WebApplicationContextUtils#getRequiredWebApplicationContext
    顺藤摸瓜来到WebApplicationContextUtils,发现getWebApplicationContext方法中只有一句话:
      return getWebApplicationContext(sc,WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);

      感觉在这个返回方法中肯定有解决我问题的答案,于是继续往下查找。

      Object attr = sc.getAttribute(attrName);
      return (WebApplicationContext) attr;

    这不就是initWebApplicationContext方法中setAttribute进去的WebApplicationContext吗?因此可以确信得到servletContext也可以得到webApplicationContext。

    那么问题又来了,通过servletContext可以得到webApplicationContext有什么意义吗? 
    上面我们提到“把创建好的springcontext,交给application内置对象,提供给监听器/过滤器/拦截器使用”。 
    假设我们有一个需求是要做首页显示。平时的代码经常是在控制器控制返回结果给前台的,那么第一页需要怎么去显示呢。抽象得到的问题是如何在一开始拿到数据。

    能想到的大致的解决方案有三种:

    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 
    1.可以通过ajx异步加载的方式请求后台数据,然后呈现出来。 
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 
    2.页面重定向的思路,先把查询请求交给控制器处理,得到查询结果后转到首页绑定数据并显示。 
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 
    3.在Ioc容器初始化的过程中,把数据查询出来,然后放在application里。 
    +++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    三种方案都能实现首页显示,不过前两种方法很大的弊端就是需要频繁操作数据库,会对数据库造成一定的压力。而同样地实现监听器逻辑的第三种方法也有弊端。就是无法实时更新,不过数据库压力相对前两种不是很大。针对无法实时更新这一问题有成熟的解决方案,可以使用定时器的思路。隔一段时间重启一次。目前来说有许多网站都是这么做的。

    而对于首页这种访问量比较大的页面,如果说最好的解决方案是实现静态化技术。

    前阵子考虑写一篇关于伪静态化的文章。当然和静态化还是有区别的。好了,回到我们listener的实现上来。

    我们说过“ContextLoaderListener实现了ServletContextListener接口。服务器启动时contextInitialized会被调用”。加载容器时能取出数据,那么我们需要实现这个接口。

    @Service
    public class CommonListener implements ServletContextListener{
    
        @Autowired
        private UserService userService;
    
        public void contextInitialized(ServletContextEvent servletContextEvent) {
            //Exception sending context initialized event to listener instance of class com.walidake.listener.CommonListener java.lang.NullPointerException
            System.out.println(userService.findUser());
        }
    
        public void contextDestroyed(ServletContextEvent servletContextEvent) {
            // TODO Auto-generated method stub
    
        }
    
    }

    需要注意一件事! 
    spring是管理逻辑层和数据访问层的依赖。而listener是web组件,那么必然不能放在spring里面。真正实例化它的应该是tomcat,在启动加载web.xml实例化的。上层的组件不可能被下层实例化得到。 
    因此,即使交给Spring实例化,它也没能力去帮你实例化。真正实现实例化的还是web容器。

    然而NullPointerException并不是来自这个原因,我们说过“ContextLoader来完成实际的WebApplicationContext,也就是Ioc容器的初始化工作”。我们并没有继承ContextLoader,没有Ioc容器的初始化,是无法实现依赖注入的。

    因此,我们想到另一种解决方案,能不能通过new ClassPathXmlApplicationContext的方式,像测试用例那样取得Ioc容器中的bean对象。

      ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-config.xml");
      userService = context.getBean(UserService.class);
      System.out.println(userService.findUser());

    发现可以正常打印出结果。然而观察日志后发现,原本的单例被创建了多次(譬如userServiceImpl等)。因此该方法并不可取。

    那么,由于被创建了多次,是不是可以说明项目中已存在了WebApplicationContext? 
    是的。我们一开始说“在初始化ContextLoaderListener成功后,spring context会存放在servletContext中”,意味着我们完全可以从servletContext取出WebApplicationContext,然后getBean取得需要的bean对象。

    所以完全可以这么做。

      ApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(servletContextEvent.getServletContext());
      userService = context.getBean(UserService.class);
      datas = userService.findUser();
      servletContextEvent.getServletContext().setAttribute("datas", datas);

    然后在jsp页面通过jstl打印出来。结果如下:

    然后在jsp页面通过jstl打印出来。结果如下:

    这里写图片描述

    显示结果正确,并且再次观察日志发现并没有初始化多次,说明猜想和实现都是正确的。

    最后,ContextLoaderListener的解析就到这里了。

  • 相关阅读:
    ZOJ 2158 Truck History
    Knight Moves (zoj 1091 poj2243)BFS
    poj 1270 Following Orders
    poj 2935 Basic Wall Maze (BFS)
    Holedox Moving (zoj 1361 poj 1324)bfs
    ZOJ 1083 Frame Stacking
    zoj 2193 Window Pains
    hdu1412{A} + {B}
    hdu2031进制转换
    openjudge最长单词
  • 原文地址:https://www.cnblogs.com/lxl57610/p/8365681.html
Copyright © 2011-2022 走看看