簡述Spring容器與SpringMVC的容器的聯(lián)系與區(qū)別

在Spring整體框架的核心概念中,容器的核心思想是管理Bean的整個生命周期侠讯。但在一個項目中挖藏,Spring容器往往不止一個,最常見的場景就是在一個項目中引入Spring和SpringMVC這兩個框架厢漩,其本質(zhì)就是兩個容器:Spring是根容器膜眠,SpringMVC是其子容器。關(guān)于這兩個容器的創(chuàng)建溜嗜、聯(lián)系及區(qū)別也正是本文所關(guān)注的問題宵膨。

一、引子

Spring和SpringMVC作為Bean管理容器和MVC層的默認(rèn)框架炸宵,已被眾多web應(yīng)用采用辟躏。但是在實際應(yīng)用中,初級開發(fā)者常常會因?qū)pring和SpringMVC的配置失當(dāng)導(dǎo)致一些奇怪的異惩寥現(xiàn)象捎琐,比如Controller的方法無法攔截、Bean被多次加載等問題涯曲,這種情況發(fā)生的根本原因在于開發(fā)者對Spring容器和SpringMVC容器之間的關(guān)系了解不夠深入,這也正是本文要闡述的問題。

二绰沥、Spring容器、SpringMVC容器與ServletContext之間的關(guān)系

在Web容器中配置Spring時贺待,你可能已經(jīng)司空見慣于web.xml文件中的以下配置代碼徽曲,下面我們以該代碼片段為基礎(chǔ)來了解Spring容器、SpringMVC容器與ServletContext之間的關(guān)系麸塞。要想理解這三者的關(guān)系秃臣,需要先熟悉Spring是怎樣在web容器中啟動起來的。Spring的啟動過程其實就是其Spring IOC容器的啟動過程。特別地奥此,對于web程序而言稚虎,IOC容器啟動過程即是建立上下文的過程撤嫩。

<web-app>

 ...

    <!-- 利用Spring提供的ContextLoaderListener監(jiān)聽器去監(jiān)聽ServletContext對象的創(chuàng)建,并初始化WebApplicationContext對象 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- Context Configuration locations for Spring XML files(默認(rèn)查找/WEB-INF/applicationContext.xml) -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <!-- 配置Spring MVC的前端控制器:DispatchcerServlet -->
    <servlet>
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>SpringMVC</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

 ...

</web-app>

Spring的啟動過程

  1. 對于一個web應(yīng)用蠢终,其部署在web容器中程奠,web容器提供其一個全局的上下文環(huán)境,這個上下文就是ServletContext祭钉,其為后面的spring IoC容器提供宿主環(huán)境梦染;
  2. 在web.xml中會提供有contextLoaderListener。在web容器啟動時朴皆,會觸發(fā)容器初始化事件帕识,此時contextLoaderListener會監(jiān)聽到這個事件,其contextInitialized方法會被調(diào)用遂铡。在這個方法中肮疗,spring會初始化一個啟動上下文,這個上下文被稱為根上下文扒接,即WebApplicationContext伪货。WebApplicationContext是一個接口類,確切的說钾怔,其實際的實現(xiàn)類是XmlWebApplicationContext碱呼,它就是spring的IoC容器,其對應(yīng)的Bean定義的配置由web.xml中的<context-param>標(biāo)簽指定宗侦。在這個IoC容器初始化完畢后愚臀,Spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE為屬性Key,將其存儲到ServletContext中矾利,便于獲裙昧选;
  3. ContextLoaderListener監(jiān)聽器初始化完畢后男旗,開始初始化web.xml中配置的Servlet舶斧,這個servlet可以配置多個,以最常見的DispatcherServlet為例察皇,這個servlet實際上是一個標(biāo)準(zhǔn)的前端控制器茴厉,用以轉(zhuǎn)發(fā)、匹配、處理每個servlet請求矾缓。DispatcherServlet上下文在初始化的時候會建立自己的IoC上下文怀酷,用以持有spring mvc相關(guān)的bean。特別地而账,在建立DispatcherServlet自己的IoC上下文前胰坟,會利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先從ServletContext中獲取之前的根上下文(即WebApplicationContext)作為自己上下文的parent上下文。有了這個parent上下文之后泞辐,再初始化自己持有的上下文笔横。這個DispatcherServlet初始化自己上下文的工作在其initStrategies方法中可以看到,大概的工作就是初始化處理器映射咐吼、視圖解析等吹缔。這個servlet自己持有的上下文默認(rèn)實現(xiàn)類也是mlWebApplicationContext。初始化完畢后锯茄,spring以與servlet的名字相關(guān)(此處不是簡單的以servlet名為Key厢塘,而是通過一些轉(zhuǎn)換,具體可自行查看源碼)的屬性為屬性Key肌幽,也將其存到ServletContext中晚碾,以便后續(xù)使用。這樣每個servlet就持有自己的上下文喂急,即擁有自己獨立的bean空間格嘁,同時各個servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定義的那些bean廊移。

Spring容器與SpringMVC的容器聯(lián)系與區(qū)別

ContextLoaderListener中創(chuàng)建Spring容器主要用于整個Web應(yīng)用程序需要共享的一些組件糕簿,比如DAO、數(shù)據(jù)庫的ConnectionFactory等狡孔;而由DispatcherServlet創(chuàng)建的SpringMVC的容器主要用于和該Servlet相關(guān)的一些組件懂诗,比如Controller、ViewResovler等苗膝。它們之間的關(guān)系如下:

作用范圍

子容器(SpringMVC容器)可以訪問父容器(Spring容器)的Bean腰耙,父容器(Spring容器)不能訪問子容器(SpringMVC容器)的Bean伶跷。也就是說腾啥,當(dāng)在SpringMVC容器中g(shù)etBean時霜旧,如果在自己的容器中找不到對應(yīng)的bean,則會去父容器中去找界阁,這也解釋了為什么由SpringMVC容器創(chuàng)建的Controller可以獲取到Spring容器創(chuàng)建的Service組件的原因。

具體實現(xiàn)

在Spring的具體實現(xiàn)上胖喳,子容器和父容器都是通過ServletContext的setAttribute方法放到ServletContext中的泡躯。但是,ContextLoaderListener會先于DispatcherServlet創(chuàng)建ApplicationContext,DispatcherServlet在創(chuàng)建ApplicationContext時會先找到由ContextLoaderListener所創(chuàng)建的ApplicationContext较剃,再將后者的ApplicationContext作為參數(shù)傳給DispatcherServlet的ApplicationContext的setParent()方法咕别。也就是說,子容器的創(chuàng)建依賴于父容器的創(chuàng)建写穴,父容器先于子容器創(chuàng)建惰拱。在Spring源代碼中,你可以在FrameServlet.java中找到如下代碼:

wac.setParent(parent);

其中啊送,wac即為由DisptcherServlet創(chuàng)建的ApplicationContext偿短,而parent則為有ContextLoaderListener創(chuàng)建的ApplicationContext。此后馋没,框架又會調(diào)用ServletContext的setAttribute()方法將wac加入到ServletContext中昔逗。

三、Spring容器和SpringMVC容器的配置

在Spring整體框架的核心概念中篷朵,容器是核心思想勾怒,就是用來管理Bean的整個生命周期的,而在一個項目中声旺,容器不一定只有一個笔链,Spring中可以包括多個容器,而且容器間有上下層關(guān)系腮猖,目前最常見的一種場景就是在一個項目中引入Spring和SpringMVC這兩個框架鉴扫,其實就是兩個容器:Spring是根容器,SpringMVC是其子容器缚够。在上文中幔妨,我們提到,SpringMVC容器可以訪問Spring容器中的Bean谍椅,Spring容器不能訪問SpringMVC容器的Bean误堡。但是,若開發(fā)者對Spring容器和SpringMVC容器之間的關(guān)系了解不夠深入雏吭,常常會因配置失當(dāng)而導(dǎo)致同時配置Spring和SpringMVC時出現(xiàn)一些奇怪的異常锁施,比如Controller的方法無法攔截、Bean被多次加載等問題杖们。

在實際工程中悉抵,一個項目中會包括很多配置,根據(jù)不同的業(yè)務(wù)模塊來劃分摘完,我們一般思路是各負(fù)其責(zé)姥饰,明確邊界,即:Spring根容器負(fù)責(zé)所有其他非controller的Bean的注冊孝治,而SpringMVC只負(fù)責(zé)controller相關(guān)的Bean的注冊列粪,下面我們演示這種配置方案审磁。

(1). Spring容器配置

Spring根容器負(fù)責(zé)所有其他非controller的Bean的注冊:

<!-- 啟用注解掃描,并定義組件查找規(guī)則 岂座,除了@controller态蒂,掃描所有的Bean -->
    <context:component-scan base-package="cn.edu.tju.rico">
        <context:exclude-filter type="annotation"
            expression="org.springframework.stereotype.Controller" />
    </context:component-scan>

(2). SpringMVC容器配置

SpringMVC只負(fù)責(zé)controller相關(guān)的Bean的注冊,其中@ControllerAdvice用于對控制器進(jìn)行增強(qiáng)费什,常用于實現(xiàn)全局的異常處理類:

    <!-- 啟用注解掃描钾恢,并定義組件查找規(guī)則 ,mvc層只負(fù)責(zé)掃描@Controller鸳址、@ControllerAdvice -->
    <!-- base-package 如果多個瘩蚪,用“,”分隔 -->
    <context:component-scan base-package="cn.edu.tju.rico"
        use-default-filters="false">
        <!-- 掃描@Controller -->
        <context:include-filter type="annotation"
            expression="org.springframework.stereotype.Controller" />
        <!--控制器增強(qiáng),使一個Contoller成為全局的異常處理類氯质,類中用@ExceptionHandler方法注解的方法可以處理所有Controller發(fā)生的異常 -->
        <context:include-filter type="annotation"
            expression="org.springframework.web.bind.annotation.ControllerAdvice" />
    </context:component-scan>

在<context:component-scan>中可以添加use-default-filters募舟,Spring配置中的use-default-filters用來指示是否自動掃描帶有@Component、@Repository闻察、@Service和@Controller的類拱礁。默認(rèn)為true,即默認(rèn)掃描辕漂。如果想要過濾其中這四個注解中的一個呢灶,比如@Repository,可以添加<context:exclude-filter />子標(biāo)簽钉嘹,如下:

<context:component-scan base-package="cn.edu.tju.rico" scoped-proxy="targetClass" use-default-filters="true"> 
 <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/> 
</context:component-scan> 

而<context:include-filter/>子標(biāo)簽是用來添加掃描注解的:

<context:component-scan base-package="cn.edu.tju.rico" scoped-proxy="targetClass" use-default-filters="false"> 
 <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/> 
</context:component-scan> 

四鸯乃、Spring容器和SpringMVC容器的配置失當(dāng)帶來的問題

問題描述

在一個項目中,想使用Spring AOP在Controller中切入一些邏輯跋涣,但發(fā)現(xiàn)不能切入到Controller的中缨睡,但可以切入到Service中。最初的配置情形如下:

1). Spring的配置文件application.xml包含了開啟AOP自動代理陈辱、Service掃描配置以及Aspect的自動掃描配置奖年,如下所示:

<aop:aspectj-autoproxy/>
<context:component-scan base-package="cn.edu.tju.rico">

2). Spring MVC的配置文件spring-mvc.xml主要內(nèi)容是Controller層的自動掃描配置

<context:component-scan base-package="cn.edu.tju.rico.controller" />

3). 增強(qiáng)代碼為如下:

@Component
@Aspect
public class SecurityAspect {
    private static final String DEFAULT_TOKEN_NAME = "X-Token";
    private TokenManager tokenManager;
    @Resource(name = "tokenManager")
    public void setTokenManager(TokenManager tokenManager) {
        this.tokenManager = tokenManager;
    }
    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public Object execute(ProceedingJoinPoint pjp) throws Throwable {
        // 從切點上獲取目標(biāo)方法
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        // 若目標(biāo)方法忽略了安全性檢查沛贪,則直接調(diào)用目標(biāo)方法
        if (method.isAnnotationPresent(IgnoreSecurity.class)) {
            System.out
                    .println("method.isAnnotationPresent(IgnoreSecurity.class) : "
                            + method.isAnnotationPresent(IgnoreSecurity.class));
            return pjp.proceed();
        }
        // 從 request header 中獲取當(dāng)前 token
        String token = WebContext.getRequest().getHeader(DEFAULT_TOKEN_NAME);
        // 檢查 token 有效性
        if (!tokenManager.checkToken(token)) {
            String message = String.format("token [%s] is invalid", token);
            throw new TokenException(message);
        }
        // 調(diào)用目標(biāo)方法
        return pjp.proceed();
    }
}

4). 需要被代理的Controller如下:

@RestController
@RequestMapping("/tokens")
public class TokenController {
    private UserService userService;
    private TokenManager tokenManager;
    public UserService getUserService() {
        return userService;
    }
    @Resource(name = "userService")
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    public TokenManager getTokenManager() {
        return tokenManager;
    }
    @Resource(name = "tokenManager")
    public void setTokenManager(TokenManager tokenManager) {
        this.tokenManager = tokenManager;
    }
    @RequestMapping(method = RequestMethod.POST)
    @IgnoreSecurity
    public Response login(@RequestParam("uname") String uname,
            @RequestParam("passwd") String passwd) {
        boolean flag = userService.login(uname, passwd);
        if (flag) {
            String token = tokenManager.createToken(uname);
            System.out.println("**** Token **** : " + token);
            return new Response().success("Login Success...");
        }
        return new Response().failure("Login Failure...");
    }
    @RequestMapping(method = RequestMethod.DELETE)
    @IgnoreSecurity
    public Response logout(@RequestParam("uname") String uname) {
        tokenManager.deleteToken(uname);
        return new Response().success("Logout Success...");
    }
}

在運行過程中陋守,發(fā)現(xiàn)這樣配置并沒有起作用,AOP配置不生效利赋,沒有生成TokenController的代理水评。

解決方案

由上一節(jié)可知,原因有兩點:

  • Spring容器與SpringMVC容器分別基于各自的配置文件進(jìn)行初始化媚送,所以中燥,在SpringMVC容器創(chuàng)建TokenController時,由于其沒有啟用AOP代理塘偎,導(dǎo)致SpringMVC容器沒有為TokenController生成代理褪那,所以沒有生效幽纷。
  • 雖然父容器啟用了AOP代理式塌,但由于父子容器的獨立性博敬,無濟(jì)于事。

因此峰尝,我們只需要在SpringMVC的配置文件中添加Aspect的自動掃描配置即可實現(xiàn)所要的效果偏窝。此外,一般地武学,SpringMVC容器只管理Controller祭往,剩下的Service、Repository 和 Component 由Spring容器只管理火窒,不建議兩個容器上在管理Bean上發(fā)生交叉硼补。因此,建議配置為:

SpringMVC 配置:

<aop:aspectj-autoproxy/>
<context:component-scan base-package="com.hodc.sdk.controller" />

Spring配置:

<context:annotation-config/>
 <context:component-scan base-package="com.hodc.sdk">
 <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
 </context:component-scan>

總結(jié)

Spring容器和SpringMVC容器雖然是父容器與子容器的關(guān)系熏矿,但二者之間具有一定的獨立性已骇。具體來說,兩個容器基于各自的配置文件分別進(jìn)行初始化票编,只有在子容器找不到對應(yīng)的Bean時褪储,才回去父容器中去找并加載

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市慧域,隨后出現(xiàn)的幾起案子鲤竹,更是在濱河造成了極大的恐慌,老刑警劉巖昔榴,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辛藻,死亡現(xiàn)場離奇詭異,居然都是意外死亡互订,警方通過查閱死者的電腦和手機(jī)吱肌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屁奏,“玉大人岩榆,你說我怎么就攤上這事》仄埃” “怎么了勇边?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長折联。 經(jīng)常有香客問我粒褒,道長,這世上最難降的妖魔是什么诚镰? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任奕坟,我火速辦了婚禮祥款,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘月杉。我一直安慰自己刃跛,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布苛萎。 她就那樣靜靜地躺著桨昙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腌歉。 梳的紋絲不亂的頭發(fā)上蛙酪,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機(jī)與錄音翘盖,去河邊找鬼桂塞。 笑死,一個胖子當(dāng)著我的面吹牛馍驯,可吹牛的內(nèi)容都是我干的阁危。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼泥彤,長吁一口氣:“原來是場噩夢啊……” “哼欲芹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起吟吝,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤菱父,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后剑逃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浙宜,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年蛹磺,在試婚紗的時候發(fā)現(xiàn)自己被綠了粟瞬。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡萤捆,死狀恐怖裙品,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情俗或,我是刑警寧澤市怎,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站辛慰,受9級特大地震影響区匠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜帅腌,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一驰弄、第九天 我趴在偏房一處隱蔽的房頂上張望麻汰。 院中可真熱鬧,春花似錦戚篙、人聲如沸五鲫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臣镣。三九已至,卻和暖如春智亮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背点待。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工阔蛉, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人癞埠。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓状原,卻偏偏與公主長得像,于是被迫代替她去往敵國和親苗踪。 傳聞我的和親對象是個殘疾皇子颠区,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351

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