1. 程式人生 > >Spring原始碼探究:容器

Spring原始碼探究:容器

結合原始碼分析 Spring 容器與 SpringMVC 容器之間的關係

問題

問題描述:專案中發現,自定義切面註解在 Controller 層正常工作,在 Service 層卻無法正常工作。為了便於分析,去掉程式碼中的業務邏輯,只留下場景。

自定義註解,列印時間

/**

* 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 />

<!-- 動態代理開啟 -->

<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 />

<!-- 動態代理開啟 -->

<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"/>

以上為主要程式碼。專案執行之後,發現在 Service 層的註解切面未生效,而在 Controller 層正常。而當我將 springmvc.xml 中的

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

遷移至 spring.xml 中,發現 Service 層與 Controller 層的註解切面均可正常執行。WHY???

從原始碼的角度探究該問題

由於原始碼中的方法較長,所以只貼出重點且與主題相關的程式碼。建議結合本地原始碼一起看。

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

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 監聽器是 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 監聽器一共有四個方法,可以很容易地判斷出來,進入該監聽器後,會進入初始化方法:contextInitialized。繼而進入initWebApplicationContext 方法,方法註釋中“Initialize Spring’s web application context for the given servlet context”,明確表明了該方法的目的是初始化 Spring Web 應用。這段程式碼中有兩句話比較關鍵:

this.context = createWebApplicationContext(servletContext);

建立 Web 應用容器,即建立了 Spring 容器;

configureAndRefreshWebApplicationContext(cwac, servletContext);

配置並重新整理Spring容器。後續發生的所有事,都是從它開始的。進入,裡面的重點程式碼是:

wac.refresh();

refresh() 方法是spring容器注入bean的核心方法,每一行程式碼都很重要。程式碼結構也非常優美,每一行程式碼背後都完成了一件事,程式碼結構比較容易理解。由於內容較多,只講裡面跟主題相關的兩句話:

ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

獲取 Bean 工廠,把你配置檔案中的內容,放在 Bean 工廠中,留著後面建立 Bean 時用。

finishBeanFactoryInitialization(beanFactory);

開始建立 Bean,即實現 Spring 中的自動注入功能。進入該方法後,末尾有這麼一句話:

beanFactory.preInstantiateSingletons();

繼續跟進,貼出該方法中的重點程式碼:

getBean(beanName);

我們在 preInstantiateSingletons() 方法中,會發現有多個地方出現了 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,若沒有取到,繼續往下走:

// 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。暫且跳過,後續會回來分析這一段。繼續:

// 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 的建立過程,此處出現了create,毫不猶豫地跟進,進入實現類中的方法,有這麼一句:

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

剛才咱們提了,Spring 中有do 命名的方法,是真正幹活的。跟進:

instanceWrapper = createBeanInstance(beanName, mbd, args);

這句話是初始化 Bean,即建立了 Bean,等價於呼叫了一個類的空構造方法。此時,已經成功地建立了物件,下文需要做的是,給該物件注入需要的屬性;

populateBean(beanName, mbd, instanceWrapper);

填充 Bean 屬性,就是剛才咱們提的,初始化一個物件後,只是一個空物件,需要給它填充屬性。跟進,看 Spring 是如何為物件注入屬性的,或者說,看一下 Spring 是如何實現 Bean 屬性的自動注入:

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

繼續進入 AutowiredAnnotationBeanPostProcessor 的 postProcessPropertyValues 方法:

metadata.inject(bean, beanName, pvs);

這句話中,出現了inject,這個詞的意思是“注入”。咱們可以斷定,Spring 的自動注入,八成跟它有關了。進入該方法:

<code>element.inject(target, beanName, pvs); </code>

與上一句一樣,只是做了一些引數處理,並沒有開始注入。繼續跟進看:

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容器已經初始化完成,Spring Bean注入的大概流程,咱們也已經熟悉了。回到開始初始化 Spring 容器的地方,ContextLoader 類 initWebApplicationContext 方法,

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

初始化 Spring 容器之後,將其放入了 servletContext 中。

咱們的問題是,“在專案中,自定義切面註解在 Controller 層正常工作,卻在 Service 層無法正常工作?”看完這個,其實並沒有解答該問題,咱們下面繼續看 SpringMVC Bean的載入流程,看完 SpringMVC 後,答案會自動浮出水面。

SpringMVC 容器 Bean 載入流程

同樣,從 web.xml 中的 SpringMVC 配置出發,裡面有 DispatcherServlet,這是 SpringMVC 的入口,跟進之後發現方法較多,無法知道會執行哪個方法。但是咱們要記住,DispatcherServlet 本質上是一個 Servlet,通過它的繼承關係圖也可以證明:

 

 

DispatcherServlet繼承關係圖

看一下 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 方法,層層跟進(一定要根據 DispatcherServlet 繼承圖跟進),進入到了 FrameworkServlet 的initServletBean() 方法,進入方法,貼出重點程式碼:

this.webApplicationContext = initWebApplicationContext();

字面理解,初始化 SpringMVC Web容器,進入探究:

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

前面咱們提到,Spring 容器初始化完成之後,放入了servletContext 中。這裡又從 servletContext 獲取到了Spring 容器

wac = createWebApplicationContext(rootContext);

字面理解建立 Web 應用容器,且引數是 Spring 容器。跟進方法:

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

建立web應用容器,即咱們所理解的 SpringMVC容器在此建立了;

wac.setParent(parent);

這裡是重點,SpringMVC 容器將 Spring 容器設定成了自己的父容器

configureAndRefreshWebApplicationContext(wac);

這個方法剛才在分析 Spring Bean 載入流程時,分析過了。其中有一段,前面說,“暫且跳過,後續會回來分析這一段”。現在開始分析:

在 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,反之則不行。

現在來解答咱們的問題

<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,所有都能正常使用它。

歡迎工作一到五年的Java工程師朋友們加入Java程式設計師開發: 854393687
群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!