每一個整合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大州,實現(xiàn)的是ServletContextListener接口续语。
繼承ContextLoader有什么作用?
ContextLoaderListener可以指定在Web應用程序啟動時載入Ioc容器摧茴,正是通過ContextLoader來實現(xiàn)的绵载,ContextLoader來完成實際的WebApplicationContext,也就是Ioc容器的初始化工作苛白。
實現(xiàn)ServletContextListener又有什么作用娃豹?
ServletContextListener接口里的函數(shù)會結合Web容器的生命周期被調(diào)用。因為ServletContextListener是ServletContext的監(jiān)聽者购裙,如果ServletContext發(fā)生變化懂版,會觸發(fā)相應的事件,而監(jiān)聽器一直對事件監(jiān)聽躏率,如果接收到了變化躯畴,就會做出預先設計好的相應動作。由于ServletContext變化而觸發(fā)的監(jiān)聽器的響應具體包括:在服務器啟動時薇芝,ServletContext被創(chuàng)建的時候蓬抄,服務器關閉時,ServletContext將被銷毀的時候等夯到。
那么ContextLoaderListener的作用是什么嚷缭?
ContextLoaderListener的作用就是啟動Web容器時,讀取在contextConfigLocation中定義的xml文件耍贾,自動裝配ApplicationContext的配置信息阅爽,并產(chǎn)生WebApplicationContext對象,然后將這個對象放置在ServletContext的屬性里荐开,這樣我們只要得到Servlet就可以得到WebApplicationContext對象付翁,并利用這個對象訪問spring容器管理的bean。
簡單來說晃听,就是上面這段配置為項目提供了spring支持百侧,初始化了Ioc容器。
那又是怎么為我們的項目提供spring支持的呢能扒?
上面說到“監(jiān)聽器一直對事件監(jiān)聽佣渴,如果接收到了變化,就會做出預先設計好的相應動作”赫粥。而監(jiān)聽器的響應動作就是在服務器啟動時contextInitialized會被調(diào)用观话,關閉的時候contextDestroyed被調(diào)用予借。這里我們關注的是WebApplicationContext如何完成創(chuàng)建越平。因此銷毀方法就暫不討論频蛔。
@Override
public void contextInitialized(ServletContextEvent event) {
//初始化webApplicationCotext</font>
initWebApplicationContext(event.getServletContext());
}
值得一提的是在initWebApplicationContext方法上面的注釋提到(請對照原注釋),WebApplicationContext根據(jù)在context-params中配置contextClass和contextConfigLocation完成初始化秦叛。有大概的了解后晦溪,接下來繼續(xù)研究源碼。
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!");
}
// 創(chuàng)建得到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);
【點擊進入該方法發(fā)現(xiàn)這樣一段:
//為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);
}
】
}
}
// 把創(chuàng)建好的spring context避咆,交給application內(nèi)置對象舟肉,提供給監(jiān)聽器/過濾器/攔截器使用
servletContext.setAttribute(
WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
this.context);
// 返回webApplicationContext
return this.context;
}
initWebApplicationContext中加載了contextConfigLocation的配置信息,初始化Ioc容器查库,說明了上述配置的必要性路媚。而我有了新的疑問。
WebApplicationContext和ServletContext是一種什么樣的關系呢樊销?
翻到源碼整慎,發(fā)現(xiàn)在ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE上面有:
org.springframework.web.context.support.WebApplicationContextUtils#getWebApplicationContext
org.springframework.web.context.support.WebApplicationContextUtils#getRequiredWebApplicationContext
順藤摸瓜來到WebApplicationContextUtils,發(fā)現(xiàn)getWebApplicationContext方法中只有一句話:
return getWebApplicationContext(sc,WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
感覺在這個返回方法中肯定有解決我問題的答案围苫,于是繼續(xù)往下查找裤园。
Object attr = sc.getAttribute(attrName);
return (WebApplicationContext) attr;
這不就是initWebApplicationContext方法中setAttribute進去的WebApplicationContext嗎?因此可以確信得到servletContext也可以得到webApplicationContext剂府。
那么問題又來了拧揽,通過servletContext可以得到webApplicationContext有什么意義嗎?
上面我們提到“把創(chuàng)建好的springcontext周循,交給application內(nèi)置對象强法,提供給監(jiān)聽器/過濾器/攔截器使用”。
假設我們有一個需求是要做首頁顯示湾笛。平時的代碼經(jīng)常是在控制器控制返回結果給前臺的饮怯,那么第一頁需要怎么去顯示呢。抽象得到的問題是如何在一開始拿到數(shù)據(jù)嚎研。
能想到的大致的解決方案有三種:
+++++++++++++++++++++++++++++++++++++++++++++++
1.可以通過ajx異步加載的方式請求后臺數(shù)據(jù)蓖墅,然后呈現(xiàn)出來。
+++++++++++++++++++++++++++++++++++++++++++++++
2.頁面重定向的思路临扮,先把查詢請求交給控制器處理论矾,得到查詢結果后轉到首頁綁定數(shù)據(jù)并顯示。
+++++++++++++++++++++++++++++++++++++++++++++++
3.在Ioc容器初始化的過程中杆勇,把數(shù)據(jù)查詢出來贪壳,然后放在application里。
+++++++++++++++++++++++++++++++++++++++++++++++
三種方案都能實現(xiàn)首頁顯示蚜退,不過前兩種方法很大的弊端就是需要頻繁操作數(shù)據(jù)庫闰靴,會對數(shù)據(jù)庫造成一定的壓力彪笼。而同樣地實現(xiàn)監(jiān)聽器邏輯的第三種方法也有弊端。就是無法實時更新蚂且,不過數(shù)據(jù)庫壓力相對前兩種不是很大配猫。針對無法實時更新這一問題有成熟的解決方案,可以使用定時器的思路杏死。隔一段時間重啟一次泵肄。目前來說有許多網(wǎng)站都是這么做的。
而對于首頁這種訪問量比較大的頁面淑翼,如果說最好的解決方案是實現(xiàn)靜態(tài)化技術腐巢。
前陣子考慮寫一篇關于偽靜態(tài)化的文章。當然和靜態(tài)化還是有區(qū)別的玄括。好了系忙,回到我們listener的實現(xiàn)上來。
我們說過“ContextLoaderListener實現(xiàn)了ServletContextListener接口惠豺。服務器啟動時contextInitialized會被調(diào)用”银还。加載容器時能取出數(shù)據(jù),那么我們需要實現(xiàn)這個接口洁墙。
@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是管理邏輯層和數(shù)據(jù)訪問層的依賴。而listener是web組件热监,那么必然不能放在spring里面捺弦。真正實例化它的應該是tomcat,在啟動加載web.xml實例化的孝扛。上層的組件不可能被下層實例化得到列吼。
因此,即使交給Spring實例化苦始,它也沒能力去幫你實例化寞钥。真正實現(xiàn)實例化的還是web容器。
然而NullPointerException并不是來自這個原因陌选,我們說過“ContextLoader來完成實際的WebApplicationContext理郑,也就是Ioc容器的初始化工作”。我們并沒有繼承ContextLoader咨油,沒有Ioc容器的初始化您炉,是無法實現(xiàn)依賴注入的。
因此役电,我們想到另一種解決方案赚爵,能不能通過new ClassPathXmlApplicationContext的方式,像測試用例那樣取得Ioc容器中的bean對象。
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring-config.xml");
userService = context.getBean(UserService.class);
System.out.println(userService.findUser());
發(fā)現(xiàn)可以正常打印出結果冀膝。然而觀察日志后發(fā)現(xiàn)膏蚓,原本的單例被創(chuàng)建了多次(譬如userServiceImpl等)废岂。因此該方法并不可取。
那么哪廓,由于被創(chuàng)建了多次强经,是不是可以說明項目中已存在了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打印出來淫痰。結果如下:
顯示結果正確,并且再次觀察日志發(fā)現(xiàn)并沒有初始化多次整份,說明猜想和實現(xiàn)都是正確的待错。
最后,ContextLoaderListener的解析就到這里了烈评。如果對上面的解析有補充的火俄,可以在評論區(qū)留言~ 蟹蟹~