问题描述 :项目中发现,自定义切面注解在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; } } 复制代码
@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 public class UserService { private Logger logger = LoggerFactory.getLogger(getClass()) @PrintTime public String serviceAspect(){ logger.info("Service层---测试切面"); return "serviceAspect"; } } 复制代码
<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" /> 复制代码
<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???
由于源码中的个方法较长,所以只贴出 重点 且与 主题相关 的代码。建议结合本地源码一起看。
web项目的入口是 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配置部分,可以看出, 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后,答案会自动浮出水面。
同样,从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,所有都能正常使用它。