Spring源码探究:容器

结合源码分析Spring容器与Springmvc容器之间的关系


问题

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

自定义注解,打印时间
/**
 * Description: 自定义打印时间的注解
 * Created by jiangwang3 on 2018/5/9.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface PrintTime {
}
注解解析器
/**
 *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层
/**
 * @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层
/**
 * @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配置文件,主要部分
    <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,所有都能正常使用它。


项目完整源码github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容