【北京】 IT技術人員面對面試、跳槽、升職等問題,如何快速成長,獲得大廠入門資格和升職加薪的籌碼?與大廠技術大牛面對面交流,解答你的疑惑。《從職場小白到技術總監成長之路:我的職場焦慮與救贖》活動鏈接:碼客
恭喜fpx,新王登基,lpl*b 我們是冠軍
原文鏈接:https://juejin.im/post/5dd2825ff265da0bc10e3606
問題
問題描述:項目中發現,自定義切面註解在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容器是如何實現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
監聽器是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,通過它的繼承關係圖也可以證明:
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,所有都能正常使用它。
(想自學習編程的小夥伴請搜索圈T社區,更多行業相關資訊更有行業相關免費視頻教程。完全免費哦!)