Spring源碼探究:容器 Day29 2018-12-19


原文出處: Fooisart
項目完整源碼github地址

結合源碼分析Spring容器與Springmvc容器之間的關系


問題

問題描述:項目中發(fā)現(xiàn)研侣,自定義切面注解在Controller層正常工作乱投,在Service層卻無法正常工作。為了便于分析亭螟,去掉代碼中的業(yè)務邏輯,只留下場景。

自定義注解,打印時間
/**
 * Description: 自定義打印時間的注解
 * Created by jiangwang3 on 2018/5/9.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface PrintTime {
}

注解解析器
/**
 *Description:打印時間注解的解析器
 * @author jiangwang
 * @date 11:28 2018/5/14
 */
@Aspect
public class PrintTimeProcessor {
    private Logger LOGGER = LoggerFactory.getLogger(getClass());
    @Pointcut("@annotation(com.foo.service.annotation.PrintTime)")
    public void printTimePoint() {
    }
    @Around("printTimePoint()")
    public Object process(ProceedingJoinPoint jp) throws Throwable{
        System.out.println();
        LOGGER.error("開始運行程序酌住。店归。。Start==>");
        Object proceed = jp.proceed();
        LOGGER.error("結束啦赂韵,運行結束==>");
        System.out.println();
        return proceed;
    }
}

Controller層
/**
 * @author jiangwang
 * @date  2018/5/14
 */
@RestController
@RequestMapping(value = "/user")
public class UserController {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Resource
    private UserService userService;
    @RequestMapping(value = "/serviceAspect", method={RequestMethod.GET})
    public  String serviceAspect(){
        return userService.serviceAspect();
    }
    @RequestMapping(value = "/controllerAspect", method={RequestMethod.GET})
    @PrintTime
    public  String name(){
        logger.info("Controller層----測試切面");
        return "controllerAspect";
    }
}

Service層
/**
 * @author jiangwang
 * @date 11:34 2018/5/14
 */
@Service
public class UserService {
    private Logger logger = LoggerFactory.getLogger(getClass())
    @PrintTime
    public String serviceAspect(){
        logger.info("Service層---測試切面");
        return "serviceAspect";
    }
}

spring.xml配置文件,主要部分
    <context:annotation-config />
    <!-- 動態(tài)代理開啟 -->
    <aop:aspectj-autoproxy proxy-target-class="true" />
    <context:component-scan base-package="com.foo" >
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    <!-- 公共配置引入 -->
    <import resource="classpath:spring/spring-config-dao.xml" />

springmvc.xml配置文件挠蛉,主要部分
    <mvc:annotation-driven />
    <mvc:default-servlet-handler />
    <!-- 動態(tài)代理開啟 -->
    <aop:aspectj-autoproxy proxy-target-class="true" />
    <!-- mvc controller -->
    <context:component-scan base-package="com.foo.web.controller" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service" />
    </context:component-scan>
    <bean class="com.foo.service.processor.PrintTimeProcessor"/>

以上為主要代碼祭示,項目運行之后,發(fā)現(xiàn)在Service層的注解切面未生效谴古,而在Controller層正常质涛。而當我將springmvc.xml中的

<bean class="com.foo.service.processor.PrintTimeProcessor"/>

遷移至spring.xml中,發(fā)現(xiàn)Service層與Controller層的注解切面均可正常運行掰担。WHY???


從源碼的角度探究該問題

由于源碼中的個方法較長汇陆,所以只貼出重點且與主題相關的代碼。建議結合本地源碼一起看带饱。

為了說清楚這個問題毡代,咱們先看一下Spring容器是如何實現(xiàn)bea自動注入(簡化版)

web項目的入口是web.xml,所以咱們從它開始勺疼。

web.xml配置文件教寂,主要部分
<!-- Spring Config -->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/spring-config.xml</param-value>
  </context-param>

  <!-- SpringMvc Config -->
  <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:spring/spring-mvc.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>


Spring容器Bean加載流程

從spring配置部分,可以看出执庐,ContextLoaderListener監(jiān)聽器是Spring容器的入口酪耕,進入該文件:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    public ContextLoaderListener() {
    }
    public ContextLoaderListener(WebApplicationContext context) {
        super(context);
    }
    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }
    @Override
    public void contextDestroyed(ServletContextEvent event) {
        closeWebApplicationContext(event.getServletContext());
        ContextCleanupListener.cleanupAttributes(event.getServletContext());
    }
}

ContextLoaderListener 監(jiān)聽器一共有四個方法,可以很容易地判斷出來轨淌,進入該監(jiān)聽器后迂烁,會進入初始化方法:contextInitialized。繼而進入initWebApplicationContext方法递鹉,方法注釋中“Initialize Spring's web application context for the given servlet context”盟步,明確表明了該方法的目的是初始化spring web應用。這段代碼中有兩句話比較關鍵:

this.context = createWebApplicationContext(servletContext);

創(chuàng)建web 應用容器躏结,即創(chuàng)建了Spring容器址芯;

configureAndRefreshWebApplicationContext(cwac, servletContext);

配置并刷新Spring容器。后續(xù)發(fā)生的所有事窜觉,都是從它開始的谷炸。進入,里面的重點代碼是:

wac.refresh();

refresh()方法是spring容器注入bean的核心方法禀挫,每一行代碼都很重要旬陡。代碼結構也非常優(yōu)美,每一行代碼背后都完成了一件事语婴,代碼結構比較容易理解描孟。由于內容較多驶睦,只講里面跟主題相關的兩句話:

ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

獲取bean工廠,把你配置文件中的內容匿醒,放在bean工廠中场航,留著后面創(chuàng)建bean時用。

finishBeanFactoryInitialization(beanFactory);

開始創(chuàng)建bean廉羔,即實現(xiàn)spring中的自動注入功能溉痢。進入該方法后,末尾有這么一句話:

beanFactory.preInstantiateSingletons();

繼續(xù)跟進憋他,貼出該方法中的重點代碼:

getBean(beanName);      

我們在preInstantiateSingletons()方法中孩饼,會發(fā)現(xiàn)有多個地方出現(xiàn)了getBean()方法,究竟咱們貼出來的是哪一句竹挡?無關緊要镀娶。跟進去之后,

@Override
    public Object getBean(String name) throws BeansException {
        return doGetBean(name, null, null, false);
    }

這里調用了doGetBean()方法揪罕,spring中只要以do命名的方法梯码,都是真正干活的。重點代碼分段貼出分析:

// Eagerly check singleton cache for manually registered singletons.
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
    if (logger.isDebugEnabled()) {
        if (isSingletonCurrentlyInCreation(beanName)) {
            logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
                    "' that is not fully initialized yet - a consequence of a circular reference");
        }
        else {
            logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
        }
    }
    bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}

直接獲取單例bean好啰,若沒有取到忍些,繼續(xù)往下走:

// Check if bean definition exists in this factory.
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
    // Not found -> check parent.
    String nameToLookup = originalBeanName(name);
    if (args != null) {
        // Delegation to parent with explicit args.
        return (T) parentBeanFactory.getBean(nameToLookup, args);
    }
    else {
        // No args -> delegate to standard getBean method.
        return parentBeanFactory.getBean(nameToLookup, requiredType);
    }
}

這一段代碼單獨看,不知所云坎怪,里面提到了一個詞:Parent罢坝。暫且跳過,后續(xù)會回來分析這一段搅窿。繼續(xù):

// Create bean instance.
if (mbd.isSingleton()) {
       sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
            @Override
             public Object getObject() throws BeansException {
                 try {
                     return createBean(beanName, mbd, args);
                  }
                  catch (BeansException ex) {
                      // Explicitly remove instance from singleton cache: It might have been put there
                      // eagerly by the creation process, to allow for circular reference resolution.
                      // Also remove any beans that received a temporary reference to the bean.
                      destroySingleton(beanName);
                      throw ex;
                 }
                }
         });
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

這段代碼中有createBean嘁酿,咱們的目的是分析bean的創(chuàng)建過程,此處出現(xiàn)了create男应,毫不猶豫地跟進闹司,進入實現(xiàn)類中的方法,有這么一句:

Object beanInstance = doCreateBean(beanName, mbdToUse, args);

剛才咱們提了沐飘,spring中有do命名的方法游桩,是真正干活的。跟進:

instanceWrapper = createBeanInstance(beanName, mbd, args);

這句話是初始化bean耐朴,即創(chuàng)建了bean借卧,等價于調用了一個類的空構造方法。此時筛峭,已經(jīng)成功地創(chuàng)建了對象铐刘,下文需要做的是,給該對象注入需要的屬性影晓;

populateBean(beanName, mbd, instanceWrapper);

填充bean屬性镰吵,就是剛才咱們提的檩禾,初始化一個對象后,只是一個空對象疤祭,需要給它填充屬性盼产。跟進,看spring是如何為對象注入屬性的勺馆,或者說戏售,看一下spring是如何實現(xiàn)bean屬性的自動注入:

pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);

繼續(xù)進入AutowiredAnnotationBeanPostProcessor的postProcessPropertyValues方法:

metadata.inject(bean, beanName, pvs);

這句話中,出現(xiàn)了inject谓传,這個詞的意思是"注入"蜈项。咱們可以斷定芹关,spring的自動注入续挟,八成跟它有關了。進入該方法:

element.inject(target, beanName, pvs);

與上一句一樣侥衬,只是做了一些參數(shù)處理诗祸,并沒有開始注入。繼續(xù)跟進看:

Field field = (Field) this.member;
ReflectionUtils.makeAccessible(field);
field.set(target, getResourceToInject(target, requestingBeanName));

看到這里轴总,大概明白了spring是如何自動注入了直颅。java反射相關的代碼,通過反射的方式給field賦值怀樟。這里的field是bean中的某一個屬性功偿,例如咱們開始時的UserController 類中的userService。getResourceToInject往堡,獲取需要賦予的值了械荷,其實這里會重新進入getBean方法,獲取bean值(例如UserController對象中需要注入userService虑灰。)吨瞎,然后賦予field。至此穆咐,spring容器已經(jīng)初始化完成颤诀,spring bean注入的大概流程,咱們也已經(jīng)熟悉了对湃⊙陆校回到開始初始化Spring容器的地方,ContextLoader類initWebApplicationContext方法拍柒,

servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

初始化spring容器之后归露,將其放入了servletContext中。

咱們的問題是斤儿,"在項目中剧包,自定義切面注解在Controller層正常工作恐锦,卻在Service層無法正常工作?"看完這個,其實并沒有解答該問題疆液,咱們下面繼續(xù)看springmvc bean的加載流程一铅,看完springmvc后,答案會自動浮出水面堕油。


Springmvc容器Bean加載流程

同樣潘飘,從web.xml中的springmvc配置出發(fā),里面有DispatcherServlet掉缺,這是springmvc的入口卜录,跟進之后發(fā)現(xiàn)方法較多,無法知道會執(zhí)行哪個方法眶明。但是咱們要記住艰毒,DispatcherServlet本質上是一個servlet,通過它的繼承關系圖也可以證明:

[圖片上傳失敗...(image-4585b5-1545214369387)]

看一下servlet的接口:

public interface Servlet {
    public void init(ServletConfig config) throws ServletException;
    public ServletConfig getServletConfig();
    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;
    public String getServletInfo();
    public void destroy();
}

從servlet接口方法中可以看出搜囱,servlet的入口是init方法丑瞧,層層跟進(一定要根據(jù)DispatcherServlet繼承圖跟進),進入到了FrameworkServlet的initServletBean()方法蜀肘,進入方法绊汹,貼出重點代碼:

this.webApplicationContext = initWebApplicationContext();

字面理解,初始化springmvc web容器扮宠,進入探究:

WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());

前面咱們提到西乖,spring容器初始化完成之后,放入了servletContext中坛增。這里又從servletContext獲取到了spring容器获雕;

wac = createWebApplicationContext(rootContext);

字面理解創(chuàng)建web應用容器,且參數(shù)是spring容器轿偎。跟進方法:

ConfigurableWebApplicationContext wac =
                (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

創(chuàng)建web應用容器典鸡,即咱們所理解的Springmvc容器在此創(chuàng)建了;

wac.setParent(parent);

這里是重點坏晦,Springmvc容器將Spring容器設置成了自己的父容器萝玷。

configureAndRefreshWebApplicationContext(wac);

這個方法剛才在分析spring bean加載流程時,分析過了昆婿。其中有一段球碉,前面說,"暫且跳過仓蛆,后續(xù)會回來分析這一段"≌龆現(xiàn)在開始分析:
在AbstractBeanFactory類doGetBean方法,有這么一段:

// Check if bean definition exists in this factory.
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
    // Not found -> check parent.
    String nameToLookup = originalBeanName(name);
    if (args != null) {
            // Delegation to parent with explicit args.
        return (T) parentBeanFactory.getBean(nameToLookup, args);
    }
    else {
        // No args -> delegate to standard getBean method.
        return parentBeanFactory.getBean(nameToLookup, requiredType);
    }
}

這里其實是在獲取父容器中的bean,若獲取到豆拨,直接拿到bean直奋,這個方法就結束了。結論:子容器可以使用父容器里的bean施禾,反之脚线,則不行。


現(xiàn)在來解答咱們的問題
<bean class="com.foo.service.processor.PrintTimeProcessor"/>

當上門這句話放在springmvc.xml中時弥搞,名為"printTimeProcessor"的bean會存在于Springmvc容器邮绿,那么Spring容器是無法獲取它的。而Service層恰巧是存在于Spring容器中攀例,所以"printTimeProcessor"切面對Service層不起作用船逮。而Controller層本身存在于Springmvc容器,所以Controller層可以正常工作粤铭。而當它放在spring.xml中時挖胃,"printTimeProcessor"是存在于Spring容器中,Springmvc容器是Spring容器的子容器承耿,子容器可以獲取到父容器的bean冠骄,所以Controller層與Service層都能獲取到該bean伪煤,所有都能正常使用它加袋。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抱既,隨后出現(xiàn)的幾起案子职烧,更是在濱河造成了極大的恐慌,老刑警劉巖防泵,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蚀之,死亡現(xiàn)場離奇詭異,居然都是意外死亡捷泞,警方通過查閱死者的電腦和手機足删,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锁右,“玉大人失受,你說我怎么就攤上這事∮缴” “怎么了拂到?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長码泞。 經(jīng)常有香客問我兄旬,道長,這世上最難降的妖魔是什么余寥? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任领铐,我火速辦了婚禮悯森,結果婚禮上,老公的妹妹穿的比我還像新娘绪撵。我一直安慰自己呐馆,他們只是感情好,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布莲兢。 她就那樣靜靜地躺著汹来,像睡著了一般。 火紅的嫁衣襯著肌膚如雪改艇。 梳的紋絲不亂的頭發(fā)上收班,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天,我揣著相機與錄音谒兄,去河邊找鬼摔桦。 笑死,一個胖子當著我的面吹牛承疲,可吹牛的內容都是我干的邻耕。 我是一名探鬼主播,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼燕鸽,長吁一口氣:“原來是場噩夢啊……” “哼兄世!你這毒婦竟也來了?” 一聲冷哼從身側響起啊研,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤御滩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后党远,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體削解,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年沟娱,在試婚紗的時候發(fā)現(xiàn)自己被綠了氛驮。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡济似,死狀恐怖矫废,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情碱屁,我是刑警寧澤磷脯,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站娩脾,受9級特大地震影響赵誓,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一俩功、第九天 我趴在偏房一處隱蔽的房頂上張望幻枉。 院中可真熱鬧,春花似錦诡蜓、人聲如沸熬甫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽椿肩。三九已至,卻和暖如春豺谈,著一層夾襖步出監(jiān)牢的瞬間郑象,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工茬末, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留厂榛,地道東北人。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓丽惭,卻偏偏與公主長得像击奶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子责掏,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內容