天天看點

阿裡跳槽來的,連Spring容器與SpringMVC的容器的差別都不知道

作者:查漏補bug
說起spring容器和SpringMVC容器,很多剛接觸spring架構的同志都有點懵,甚至是一頭霧水,分不清楚兩者的關系和差別。這倆容器呢雖然有必然的聯系,但是他們的差別也是有的。下面我就簡單描述下。

一、前言

首先在我們開發中會與到各種各樣的容器,今天我們就說一下spring 容器與springmvc容器。Spring和SpringMVC作為Bean管理容器和MVC層的預設架構,已被衆多web應用采用。但是在實際應用中,初級開發者常常會因對Spring和SpringMVC的配置失當導緻一些奇怪的異常現象,比如Controller的方法無法攔截、Bean被多次加載等問題,這種情況發生的根本原因在于開發者對Spring容器和SpringMVC容器之間的關系了解不夠深入。

在Spring整體架構的核心概念中,容器的核心思想是管理Bean的整個生命周期。但在一個項目中,Spring容器往往不止一個,最常見的場景就是在一個項目中引入Spring和SpringMVC這兩個架構,其本質就是兩個容器。首先 springmvc和spring它倆都是容器,容器就是管理對象的地方,例如Tomcat,就是管理servlet對象的,而springMVC容器和spring容器,就是管理bean對象的地方,再說的直白點,springmvc就是管理controller對象的容器,spring就是管理service和dao的容器,這下你明白了吧。是以我們在springmvc的配置檔案裡配置的掃描路徑就是controller的路徑,而spring的配置檔案裡自然配的就是service和dao的路徑

SpringMVC.xml檔案所配置的路徑:

<context:component-scan base-package="com.jd.controller"/>
           

applicationContext-service.xml檔案所配置的路徑:

<!--開啟注解掃描 掃描dao層和service層-->
<context:component-scan base-package="com.gx.dao,com.jd.service">
    <context:include-filter expression="org.springframework.stereotype.Service" type="annotation"/>
    <context:include-filter expression="org.springframework.stereotype.Repository" type="annotation"/>
</context:component-scan>
           

其次, spring容器和springmvc容器的關系是父子容器的關系。spring容器是父容器,springmvc是子容器。在子容器裡可以通路父容器裡的對象,但是在父容器裡不可以通路子容器的對象,說的通俗點就是,在controller裡可以通路service對象,但是在service裡不可以通路controller對象

  是以這麼看的話,所有的bean,都是被spring或者springmvc容器管理的,他們可以直接注入。然後springMVC的攔截器也是springmvc容器管理的,是以在springmvc的攔截器裡,可以直接注入bean對象。

<!--SpringMVC 攔截器配置(可以多個)--> 
<mvc:interceptors> 
<!--配置攔截器--> 
<mvc:interceptor> 
<!--配置攔截的資源--> 
<mvc:mapping path="/**"/> 
<!--配置攔截器bean對象--> 
<bean class="com.jd.interceptor.MyInterceptor1"/> 
</mvc:interceptor> 
</mvc:interceptors>
           

二、Spring容器、SpringMVC容器與ServletContext之間的關系

在Web容器中配置Spring時,你可能已經司空見慣于web.xml檔案中的以下配置代碼,下面我們以該代碼片段為基礎來了解Spring容器、SpringMVC容器與ServletContext之間的關系。要想了解這三者的關系,需要先熟悉Spring是怎樣在web容器中啟動起來的。Spring的啟動過程其實就是其Spring IOC容器的啟動過程。特别地,對于web程式而言,IOC容器啟動過程即是建立上下文的過程。

<web-app>

 ...

    <!-- 利用Spring提供的ContextLoaderListener監聽器去監聽ServletContext對象的建立,并初始化WebApplicationContext對象 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- Context Configuration locations for Spring XML files(預設查找/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應用,其部署在web容器中,web容器提供其一個全局的上下文環境,這個上下文就是ServletContext,其為後面的spring IoC容器提供宿主環境;
  2. 在web.xml中會提供有contextLoaderListener。在web容器啟動時,會觸發容器初始化事件,此時contextLoaderListener會監聽到這個事件,其contextInitialized方法會被調用。在這個方法中,spring會初始化一個啟動上下文,這個上下文被稱為根上下文,即WebApplicationContext。WebApplicationContext是一個接口類,确切的說,其實際的實作類是XmlWebApplicationContext,它就是spring的IoC容器,其對應的Bean定義的配置由web.xml中的<context-param>标簽指定。在這個IoC容器初始化完畢後,Spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE為屬性Key,将其存儲到ServletContext中,便于擷取;
  3. ContextLoaderListener監聽器初始化完畢後,開始初始化web.xml中配置的Servlet,這個servlet可以配置多個,以最常見的DispatcherServlet為例,這個servlet實際上是一個标準的前端控制器,用以轉發、比對、處理每個servlet請求。DispatcherServlet上下文在初始化的時候會建立自己的IoC上下文,用以持有spring mvc相關的bean。特别地,在建立DispatcherServlet自己的IoC上下文前,會利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先從ServletContext中擷取之前的根上下文(即WebApplicationContext)作為自己上下文的parent上下文。有了這個parent上下文之後,再初始化自己持有的上下文。這個DispatcherServlet初始化自己上下文的工作在其initStrategies方法中可以看到,大概的工作就是初始化處理器映射、視圖解析等。這個servlet自己持有的上下文預設實作類也是mlWebApplicationContext。初始化完畢後,spring以與servlet的名字相關(此處不是簡單的以servlet名為Key,而是通過一些轉換,具體可自行檢視源碼)的屬性為屬性Key,也将其存到ServletContext中,以便後續使用。這樣每個servlet就持有自己的上下文,即擁有自己獨立的bean空間,同時各個servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定義的那些bean。

Spring容器與SpringMVC的容器聯系與差別

ContextLoaderListener中建立Spring容器主要用于整個Web應用程式需要共享的一些元件,比如DAO、資料庫的ConnectionFactory等;而由DispatcherServlet建立的SpringMVC的容器主要用于和該Servlet相關的一些元件,比如Controller、ViewResovler等。它們之間的關系如下:

作用範圍

子容器(SpringMVC容器)可以通路父容器(Spring容器)的Bean,父容器(Spring容器)不能通路子容器(SpringMVC容器)的Bean。也就是說,當在SpringMVC容器中getBean時,如果在自己的容器中找不到對應的bean,則會去父容器中去找,這也解釋了為什麼由SpringMVC容器建立的Controller可以擷取到Spring容器建立的Service元件的原因。

具體實作

在Spring的具體實作上,子容器和父容器都是通過ServletContext的setAttribute方法放到ServletContext中的。但是,ContextLoaderListener會先于DispatcherServlet建立ApplicationContext,DispatcherServlet在建立ApplicationContext時會先找到由ContextLoaderListener所建立的ApplicationContext,再将後者的ApplicationContext作為參數傳給DispatcherServlet的ApplicationContext的setParent()方法。也就是說,子容器的建立依賴于父容器的建立,父容器先于子容器建立。在Spring源代碼中,你可以在FrameServlet.java中找到如下代碼:

wac.setParent(parent);
           

其中,wac即為由DisptcherServlet建立的ApplicationContext,而parent則為有ContextLoaderListener建立的ApplicationContext。此後,架構又會調用ServletContext的setAttribute()方法将wac加入到ServletContext中。

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

在Spring整體架構的核心概念中,容器是核心思想,就是用來管理Bean的整個生命周期的,而在一個項目中,容器不一定隻有一個,Spring中可以包括多個容器,而且容器間有上下層關系,目前最常見的一種場景就是在一個項目中引入Spring和SpringMVC這兩個架構,其實就是兩個容器:Spring是根容器,SpringMVC是其子容器。在上文中,我們提到,SpringMVC容器可以通路Spring容器中的Bean,Spring容器不能通路SpringMVC容器的Bean。但是,若開發者對Spring容器和SpringMVC容器之間的關系了解不夠深入,常常會因配置失當而導緻同時配置Spring和SpringMVC時出現一些奇怪的異常,比如Controller的方法無法攔截、Bean被多次加載等問題。

在實際工程中,一個項目中會包括很多配置,根據不同的業務子產品來劃分,我們一般思路是各負其責,明确邊界,即:Spring根容器負責所有其他非controller的Bean的注冊,而SpringMVC隻負責controller相關的Bean的注冊,下面我們示範這種配置方案。

(1). Spring容器配置

Spring根容器負責所有其他非controller的Bean的注冊:

<!-- 啟用注解掃描,并定義元件查找規則 ,除了@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隻負責controller相關的Bean的注冊,其中@ControllerAdvice用于對控制器進行增強,常用于實作全局的異常處理類:

<!-- 啟用注解掃描,并定義元件查找規則 ,mvc層隻負責掃描@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" />
        <!--控制器增強,使一個Contoller成為全局的異常處理類,類中用@ExceptionHandler方法注解的方法可以處理所有Controller發生的異常 -->
        <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的類。預設為true,即預設掃描。如果想要過濾其中這四個注解中的一個,比如@Repository,可以添加<context:exclude-filter />子标簽,如下:

<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/>子标簽是用來添加掃描注解的:

<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容器的配置失當帶來的問題

問題描述

在一個項目中,想使用Spring AOP在Controller中切入一些邏輯,但發現不能切入到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主要内容是Controller層的自動掃描配置。

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

3). 增強代碼為如下:

@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 {
        // 從切點上擷取目标方法
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        Method method = methodSignature.getMethod();
        // 若目标方法忽略了安全性檢查,則直接調用目标方法
        if (method.isAnnotationPresent(IgnoreSecurity.class)) {
            System.out
                    .println("method.isAnnotationPresent(IgnoreSecurity.class) : "
                            + method.isAnnotationPresent(IgnoreSecurity.class));
            return pjp.proceed();
        }
        // 從 request header 中擷取目前 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);
        }
        // 調用目标方法
        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...");
    }
}
           

在運作過程中,發現這樣配置并沒有起作用,AOP配置不生效,沒有生成TokenController的代理。

解決方案

由上一節可知,原因有兩點:

  • Spring容器與SpringMVC容器分别基于各自的配置檔案進行初始化,是以,在SpringMVC容器建立TokenController時,由于其沒有啟用AOP代理,導緻SpringMVC容器沒有為TokenController生成代理,是以沒有生效。
  • 雖然父容器啟用了AOP代理,但由于父子容器的獨立性,無濟于事。

是以,我們隻需要在SpringMVC的配置檔案中添加Aspect的自動掃描配置即可實作所要的效果。此外,一般地,SpringMVC容器隻管理Controller,剩下的Service、Repository 和 Component 由Spring容器隻管理,不建議兩個容器上在管理Bean上發生交叉。是以,建議配置為:

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>
           

總結

springmvc容器是spring容器的子容器,但是子容器可以父容器的bean, 但是父容器不能通路子容器的bean。Spring容器和SpringMVC容器雖然是父容器與子容器的關系,但二者之間具有一定的獨立性。具體來說,兩個容器基于各自的配置檔案分别進行初始化,隻有在子容器找不到對應的Bean時,才回去父容器中去找并加載

寫在最後

希望通過這篇文章能讓大家厘清楚spring容器和SpringMVC容器的關系與差別。雖然這些知識點都是老生常談,不否認還是很多人分不清和不了解,但知識點不管是不是老生常談也都是需要掌握的,畢竟基礎要打好,這樣才能有更高的成就賺到更多的錢呀。好了本文到此結束了,希望能對鐵子們有幫助和收獲。喜歡的鐵子們可以點點贊和關注, 文章持續更新,也可以評論出你想看哪一塊技術。鐵子們的支援是我的動力,創作離不開鐵子們的支援,在此先感謝大家!