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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。