1. 程式人生 > >Spring中ContextLoaderListener作用

Spring中ContextLoaderListener作用

每一個整合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</font>
    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打印出來。結果如下:

QQ圖片20160827182736.png

顯示結果正確,並且再次觀察日誌發現並沒有初始化多次,說明猜想和實現都是正確的。



作者:walidake
連結:https://www.jianshu.com/p/523bfddf0810
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。