從Spring源碼角度分析bug

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

問題

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

自定義註解,打印時間

/**
 * Description: 自定義打印時間的註解
 */

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface PrintTime {

}

註解解析器

/**
 *Description:打印時間註解的解析器
 */
@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層

@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層

@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 的自動注入,八成跟它有關了。進入該方法:

element.inject(target, beanName, pvs); 

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

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,通過它的繼承關係圖也可以證明。

 

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

 

 

原文鏈接:https://mp.weixin.qq.com/s/hJX9-lc4q2Uoc3eJNPHFdw

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章