Spring源碼探究:容器


問題

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

自定義註解,打印時間
/**
 * 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中的web

<bean class="com.foo.service.processor.PrintTimeProcessor"/>
複製代碼

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

從源碼的角度探究該問題

因爲源碼中的個方法較長,因此只貼出重點且與主題相關的代碼。建議結合本地源碼一塊兒看。express

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

web項目的入口是web.xml,因此我們從它開始。spring-mvc

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容器的入口,進入該文件:bash

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容器;mvc

configureAndRefreshWebApplicationContext(cwac, servletContext);
複製代碼

配置並刷新Spring容器。後續發生的全部事,都是從它開始的。進入,裏面的重點代碼是:app

wac.refresh();
複製代碼

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

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,經過它的繼承關係圖也能夠證實:

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


BLOG地址www.liangsonghua.me

關注微信公衆號:松花皮蛋的黑板報,獲取更多精彩!

公衆號介紹:分享在京東工做的技術感悟,還有JAVA技術和業內最佳實踐,大部分都是務實的、能看懂的、可復現的

相關文章
相關標籤/搜索