Spring源碼探究:容器

問題

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

自定義註解,打印時間

1
2
3
4
5
6
7
8
9
/**
* Description: 自定義打印時間的註解
* Created by jiangwang3 on 2018/5/9.
*/
@Retention (RetentionPolicy.RUNTIME)
@Target ({ElementType.METHOD})
@Documented
public @interface PrintTime {
}

註解解析器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
*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層

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @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層

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @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 配置文件,主要部分

1
2
3
4
5
6
7
8
< 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 配置文件,主要部分

1
2
3
4
5
6
7
8
9
10
< 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 中的java

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

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

從源碼的角度探究該問題

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

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

Web 項目的入口是 web.xml,因此我們從它開始。express

web.xml 配置文件,主要部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 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 容器的入口,進入該文件:spring-mvc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 方法,方法註釋中 bash

「Initialize Spring’s web application context for the given servlet context」

,明確代表了該方法的目的是初始化 Spring Web 應用。這段代碼中有兩句話比較關鍵:架構

1
this .context = createWebApplicationContext(servletContext);

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

1
configureAndRefreshWebApplicationContext(cwac, servletContext);

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

1
wac.refresh();

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

1
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

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

1
finishBeanFactoryInitialization(beanFactory);

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

1
beanFactory.preInstantiateSingletons();

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

1
getBean(beanName);

咱們在 preInstantiateSingletons() 方法中,會發現有多個地方出現了 getBean() 方法,究竟我們貼出來的是哪一句?可有可無。跟進去以後,

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

這裏調用了 doGetBean() 方法,Spring 中只要以 do 命名的方法,都是真正幹活的。重點代碼分段貼出分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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,若沒有取到,繼續往下走:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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。

暫且跳過,後續會回來分析這一段。

繼續:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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,堅決果斷地跟進,進入實現類中的方法,有這麼一句:

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

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

1
instanceWrapper = createBeanInstance(beanName, mbd, args);

這句話是初始化 Bean,即建立了 Bean,等價於調用了一個類的空構造方法。此時,已經成功地建立了對象,下文須要作的是,給該對象注入須要的屬性;

1
populateBean(beanName, mbd, instanceWrapper);

填充 Bean 屬性,就是剛纔我們提的,初始化一個對象後,只是一個空對象,須要給它填充屬性。跟進,看 Spring 是如何爲對象注入屬性的,或者說,看一下 Spring 是如何實現 Bean 屬性的自動注入:

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

繼續進入 AutowiredAnnotationBeanPostProcessor 的 postProcessPropertyValues 方法:

1
metadata.inject(bean, beanName, pvs);

這句話中,出現了 inject,這個詞的意思是「注入」。我們能夠判定,Spring 的自動注入,八成跟它有關了。進入該方法:

<code>element.inject(target, beanName, pvs); </code>複製代碼

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

1
2
3
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 方法,

1
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 的接口:

1
2
3
4
5
6
7
8
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() 方法,進入方法,貼出重點代碼:

1
this .webApplicationContext = initWebApplicationContext();

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

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

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

1
wac = createWebApplicationContext(rootContext);

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

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

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

1
wac.setParent(parent);

這裏是重點,SpringMVC 容器將 Spring 容器設置成了本身的父容器

1
configureAndRefreshWebApplicationContext(wac);

這個方法剛纔在分析 Spring Bean 加載流程時,分析過了。其中有一段,前面說,

「暫且跳過,後續會回來分析這一段」

。如今開始分析:

在 AbstractBeanFactory 類 doGetBean 方法,有這麼一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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,反之則不行。

如今來解答我們的問題

1
< 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架構交流: 855835163
加羣連接:jq.qq.com/?_wv=1027&a…​​​​​​​羣內提供免費的架構資料還有:Java工程化、高性能及分佈式、高性能、深刻淺出。高架構。性能調優、Spring,MyBatis,Netty源碼分析和大數據等多個知識點高級進階乾貨的免費直播講解 能夠進來一塊兒學習交流哦

相關文章
相關標籤/搜索