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大州,實現(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打印出來淫痰。結果如下:

QQ圖片20160827182736.png

顯示結果正確,并且再次觀察日志發(fā)現(xiàn)并沒有初始化多次整份,說明猜想和實現(xiàn)都是正確的待错。

最后,ContextLoaderListener的解析就到這里了烈评。如果對上面的解析有補充的火俄,可以在評論區(qū)留言~ 蟹蟹~

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市讲冠,隨后出現(xiàn)的幾起案子瓜客,更是在濱河造成了極大的恐慌,老刑警劉巖竿开,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谱仪,死亡現(xiàn)場離奇詭異,居然都是意外死亡否彩,警方通過查閱死者的電腦和手機疯攒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來列荔,“玉大人卸例,你說我怎么就攤上這事〖∫悖” “怎么了筷转?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長悬而。 經(jīng)常有香客問我呜舒,道長,這世上最難降的妖魔是什么笨奠? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任袭蝗,我火速辦了婚禮唤殴,結果婚禮上,老公的妹妹穿的比我還像新娘到腥。我一直安慰自己朵逝,他們只是感情好,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布乡范。 她就那樣靜靜地躺著配名,像睡著了一般。 火紅的嫁衣襯著肌膚如雪晋辆。 梳的紋絲不亂的頭發(fā)上渠脉,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天,我揣著相機與錄音瓶佳,去河邊找鬼芋膘。 笑死,一個胖子當著我的面吹牛霸饲,可吹牛的內(nèi)容都是我干的为朋。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼厚脉,長吁一口氣:“原來是場噩夢啊……” “哼潜腻!你這毒婦竟也來了?” 一聲冷哼從身側響起器仗,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤融涣,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后精钮,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體威鹿,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年轨香,在試婚紗的時候發(fā)現(xiàn)自己被綠了忽你。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡臂容,死狀恐怖科雳,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情脓杉,我是刑警寧澤糟秘,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站球散,受9級特大地震影響尿赚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一凌净、第九天 我趴在偏房一處隱蔽的房頂上張望悲龟。 院中可真熱鬧,春花似錦冰寻、人聲如沸须教。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽轻腺。三九已至,卻和暖如春秒旋,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背诀拭。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工迁筛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耕挨。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓细卧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筒占。 傳聞我的和親對象是個殘疾皇子贪庙,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容