2019独角兽企业重金招聘Python工程师标准>>>

问题

问题描述:项目中发现,自定义切面注解在 Controller 层正常工作,在 Service 层却无法正常工作。为了便于分析,去掉代码中的业务逻辑,只留下场景。

自定义注解,打印时间

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 中的

1

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

迁移至 spring.xml 中,发现 Service 层与 Controller 层的注解切面均可正常运行。WHY???


从源码的角度探究该问题

由于源码中的方法较长,所以只贴出重点且与主题相关的代码。建议结合本地源码一起看。

为了说清楚这个问题,咱们先看一下Spring容器是如何实现 Bean 自动注入(简化版)

Web 项目的入口是 web.xml,所以咱们从它开始。

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 容器的入口,进入该文件:

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 方法,方法注释中 “Initialize Spring’s web application context for the given servlet context”,明确表明了该方法的目的是初始化 Spring Web 应用。这段代码中有两句话比较关键:

1

this.context = createWebApplicationContext(servletContext);

创建 Web 应用容器,即创建了 Spring 容器;

1

configureAndRefreshWebApplicationContext(cwac, servletContext);

配置并刷新Spring容器。后续发生的所有事,都是从它开始的。进入,里面的重点代码是:

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
加群链接:https://jq.qq.com/?_wv=1027&k=5dPqXGI
群内提供免费的架构资料还有:Java工程化、高性能及分布式、高性能、深入浅出。高架构。性能调优、Spring,MyBatis,Netty源码分析和大数据等多个知识点高级进阶干货的免费直播讲解  可以进来一起学习交流哦

转载于:https://my.oschina.net/u/3959468/blog/3016169

Spring源码探究:容器相关推荐

  1. 从Spring源码探究AOP代码织入的过程

    随着不断地使用Spring,以及后续的Boot.cloud,不断的体会到这个拯救Java的生态体系的强大,也使我对于这个框架有了极大的好奇心,以至于产生了我为什么不能写一个这样的框架的思考. 通过自学 ...

  2. 从Spring源码探究DI属性注入流程

    随着不断地使用Spring,以及后续的Boot.cloud,不断的体会到这个拯救Java的生态体系的强大,也使我对于这个框架有了极大的好奇心,以至于产生了我为什么不能写一个这样的框架的思考. 通过自学 ...

  3. 从Spring源码探究IOC初始化流程

    随着不断地使用Spring,以及后续的Boot.cloud,不断的体会到这个拯救Java的生态体系的强大,也使我对于这个框架有了极大的好奇心,以至于产生了我为什么不能写一个这样的框架的思考. 通过自学 ...

  4. Spring源码-AnnotationConfigApplicationContext容器的创建过程

    Spring提供了多种IOC容器用来管理bean,其中最常见的就是AnnotationConfigApplicationContext AnnotationConfigApplicationConte ...

  5. Spring源码——容器扩展ApplicationContext

    前言 内容主要参考自<Spring源码深度解析>一书,算是读书笔记或是原书的补充.进入正文后可能会引来各种不适,毕竟阅读源码是件极其痛苦的事情. 本文主要涉及书中第六章的部分,依照书中内容 ...

  6. spring源码 — 一、IoC容器初始化

    IoC容器初始化 注意:本次的spring源码是基于3.1.1.release版本 容器:具有获取Bean功能--这是最基本功能,也是BeanFactory接口定义的主要行为,在添加了对于资源的支持之 ...

  7. spring源码阅读(3)-- 容器启动之BeanFactoryPostProcessor

    接着上文<spring源码阅读(2)-- 容器启动之加载BeanDefinition>,当spring加载完所有BeanDefinition时,并不会马上去创建bean,而是先配置bean ...

  8. 【spring源码分析】IOC容器初始化(二)

    前言:在[spring源码分析]IOC容器初始化(一)文末中已经提出loadBeanDefinitions(DefaultListableBeanFactory)的重要性,本文将以此为切入点继续分析. ...

  9. 转 Spring源码剖析——核心IOC容器原理

    Spring源码剖析--核心IOC容器原理 2016年08月05日 15:06:16 阅读数:8312 标签: spring 源码 ioc 编程 bean 更多 个人分类: Java https:// ...

  10. Ioc容器beanDefinition-Spring 源码系列(1)

    Ioc容器beanDefinition-Spring 源码系列(1) 目录: Ioc容器beanDefinition-Spring 源码(1) Ioc容器依赖注入-Spring 源码(2) Ioc容器 ...

最新文章

  1. 如果不知道MySQL当前使用配置文件(my.cnf)的路径的解决方法
  2. Android各层推荐开发书籍及参考资料
  3. Linux系统灾难恢复技术和方法-[3]
  4. Gitlab 项目上传
  5. 第三篇:POSIX标准中的 “ 限制 ”
  6. 真正优秀的人,都有这3种习惯
  7. oracle数据块dump信息,从数据块的dump信息能看出什么
  8. 防止***根据PHP中的错误信息爆路径
  9. web安全day35:Linux防火墙进阶
  10. 7-17 mmh学长的三色灯 (20分)
  11. 服务器虚拟计算节点,什么是云服务器计算节点
  12. mac版本 sadptool_海康 设备 发现(SADPTool原理)
  13. Fences中关于Explorer非正常关闭问题
  14. mysql graler_安装Linux后常用的操作以及踩坑记录
  15. 语音识别学习记录 [kaldi中的openfst]
  16. 二、CString、CStringA 和 CStringW
  17. 嫌疑人x的献身-----一些想法
  18. shell 学习笔记---运算符
  19. 信息论与编码:C语言实现lz78算法
  20. 基于SSM婚恋网交友平台

热门文章

  1. createrepo命令
  2. Atitit.java expression fsm 表达式词法分析引擎 v2 qaa.docx
  3. wordpress页面前端添加编辑按钮
  4. 加锁解锁PHP实现 -转载
  5. [FZYZOJ 1889] 厨房救济
  6. GUI练习——列出指定目录内容
  7. SuperMap 办公自动化服务平台
  8. svchost 100%怎么办==》win
  9. logging模块(二十六)
  10. netty使用(5)client_server一发一回阐释ByteBuffer的使用