首页 > 百科知识 正文

2w字长文给你讲透了配置类为什么要添加(configure配置类)

时间:2023-11-21 12:52:39 阅读:513 作者:我爱杨阳洋

文章来源:https://mp.weixin.qq.com/s/5UvbeEnZBS7niAJw_f-6pQ

原文作者:程序员DMZ

Spring 用的爽不爽?在你爽的同时,你也知道为什么这么爽,在 Spring 中,@Configuration 是一个重重重要的注解,那么配置类为什么要添加 @Configuration 注解呢?本篇文章就带你 get 这个点。

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第1张

不加 @Configuration 导致的问题

我们先来看看如果不在配置类上添加 @Configuration 注解会有什么问题,代码示例如下:

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第2张

不添加 @Configuration 注解运行结果:

create dmzService create A by dmzService create dmzService

添加 @Configuration 注解运行结果:

create dmzService create A by dmzService

在上面的例子中,我们会发现没有添加 @Configuraion注解时dmzService被创建了两次。

  • 这是因为第一次创建是被 Spring 容器所创建的,Spring 调用这个 dmzService() 创建了一个 Bean 被放入了单例池中(没有添加其它配置默认是单例的)。
  • 第二次创建是 Spring 容器在创建 a 时调用了a(),而 a() 又调用了 dmzService() 方法。

这样的话,就出现问题了。

第一,对于 dmzService 而言,它被创建了两次,打破了单例的条件。

第二,对于 a 而言,它所依赖的 dmzService 不是 Spring 所管理的,而是直接调用的一个普通的 Java 方法创建的普通对象。这个对象没有被 Spring 对象管理,首先它的域(Scope)定义失效了其次它没有经过一个完整的生命周期,那么我们所定义所有的 Bean 的后置处理器都没有作用到它身上,其中就包括了完成 AOP 的后置处理器,所以 AOP 也失效了

上面的分析不能说服你的话,我们可以看看官方在 @Bean 上给出的这一段注释

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第3张

首先,Spring 就在注释中指出了,通常来说,BeanMethod 一般都声明在一个由 @Configuration 注解标注的类中,在这种情况下,BeanMethod 可能直接引用了在同一个类中申明的 beanMethod , 就像本文给出的例子那样,a() 直接引用了 dmzService(),我们重点再看看划红线的部分,通过调用另外一个 beanMethod 进入的 Bean的引用会被保证是遵从域定义以及 AOP 语义的,就像 getBean 所做的那样。这是怎么实现的呢?在最后被红线标注的地方也有说明,是通过在运行时期为没有被 @Configuration 注解标注的配置类生成一个 CGLIB 的子类。

源码分析

那么,Spring 是在什么时候创建的代理呢?到目前为止我们应该没有进入 Spring 启动流程的任何关键代码,那么我们不妨带着这个问题继续往下看。目前来说我们已经阅读到了Spring执行流程图中的3-5步,也就是org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors方法。其执行逻辑如下:

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第4张

在之前的分析中我们已经知道了,这个方法的主要作用就是执行 BeanFactoryPostProcessor 中的方法,首先执行的是 BeanDefinitionRegistryPostProcessor(继承了BeanFactoryPostProcessor)的postProcessBeanDefinitionRegistry方法,然后执行postProcessBeanFactory方法。

那么目前为止容器中有哪些 BeanFactoryPostProcessor 呢?

Spring 内置的 BeanFactoryPostProcessor 在当前这个时机就已经被注册到容器中的只有一个,就是 ConfigurationClassPostProcessor,在之前的文章中我们已经分析过了它的postProcessBeanDefinitionRegistry方法,这个方法主要是为了完成配置类的解析以及对组件的扫描

紧接着我们就来看看它的postProcessBeanFactory方法做了什么。其源码如下:

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第5张

enhanceConfigurationClasses

接下来我们来分析一下 enhanceConfigurationClasses 方法

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第6张

这段代码非常简单,其目的就是生成一个 enhancedClass(经过了 cglib 增强的 class),然后用其替换目标配置类对应的 BeanDefinition 中的 beanClass 属性。

那么我们接下来需要分析的就是 enhancedClass 是如何生成的。它的核心代码在ConfigurationClassEnhancer中,所以我们要分析下ConfigurationClassEnhancer的源码,在分析它的源码前,我们需要对 cglib 有一定的了解。

1、cglib原理分析

在分析 cglib 前,我们先通过一个例子来认识一下什么是 cglib。

1.1、使用示例

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第7张

运行结果为:

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第8张

可以看到,通过上面这种方式,我们已经对 Target 中的方法完成了增强(可以在方法执行前后插入我们自己的定制的逻辑)

1.2、原理分析

查看 F 盘的 code 目录,会发现多了以下几个文件

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第9张

其中第二个文件就是我们的代理类字节码,将其直接用 IDEA 打开

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第10张

从上面的代码中我们可以看出

  1. 代理类继承了目标类
  2. 目标类中的方法在代理类中对应了两个方法,就以上面例子中的目标类中的 g() 方法为例,其对应的代理类中的两个方法为
  • CGLIB0()
  • g()

它们之间的关系如下所示:

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第11张

实际被增强的方法是代理对象中的g方法,当它被调用时,在执行的过程中会调用CGLIB$g$0()方法,而这个方法又会调用代理类的父类,也就是目标类中的g()。

从这里就能看出,和 JDK 动态代理不同的是,cglib 代理采用的是继承的方式生成的代理对象。

在上面的例子中,我们实现了对 cglib 中方法的拦截,但是就目前而言我们没有办法选择性的拦截目标类中的某一个方法。

假设现在我们只想拦截 Target 中的 g 方法而不拦截 f 方法,该怎样做呢?我们看下面这个例子

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第12张

运行结果:

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第13张

此时 f 方法已经不会被代理了

2、ConfigurationClassEnhancer源码分析

2.1、创建代理过程分析

在对 cglib 的原理有了一定了解后,我们再来看ConfigurationClassEnhancer的源码就轻松多了

我们就关注其中核心的几个方法,代码如下:

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第14张

并且我们会发现,在最开始这个类就申明了三个拦截器

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第15张

2.2、拦截器源码分析

基于我们之前对 cglib 的学习,肯定能知道,代理的核心逻辑就是依赖于拦截器实现的。其中NoOp.INSTANCE代表什么都没做,我们就关注前面两个。

BeanFactoryAwareMethodInterceptor

之所以把这个拦截器放到前面分析是因为这个拦截器的执行时机是在创建配置类的时候,其源码如下:

private static class BeanFactoryAwareMethodInterceptor implements MethodInterceptor, ConditionalCallback { @Override @Nullable public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 在生成代理类的字节码时,使用了BeanFactoryAwareGeneratorStrategy策略 // 这个策略会在代理类中添加一个字段,BEAN_FACTORY_FIELD = "$$beanFactory" Field field = ReflectionUtils.findField(obj.getClass(), BEAN_FACTORY_FIELD); Assert.state(field != null, "Unable to find generated BeanFactory field"); // 此时调用的方法是setBeanFactory方法, // 直接通过反射将beanFactory赋值给BEAN_FACTORY_FIELD字段 field.set(obj, args[0]); // Does the actual (non-CGLIB) superclass implement BeanFactoryAware? // If so, call its setBeanFactory() method. If not, just exit. // 如果目标配置类直接实现了BeanFactoryAware接口,那么直接调用目标类的setBeanFactory方法 if (BeanFactoryAware.class.isAssignableFrom(ClassUtils.getUserClass(obj.getClass().getSuperclass()))) { return proxy.invokeSuper(obj, args); } return null; } @Override // 在调用setBeanFactory方法时才会拦截 // 从前文我们知道,代理类是实现了实现EnhancedConfiguration接口的, // 这就意味着它也实现了BeanFactoryAware接口,那么在创建配置类时, // setBeanFactory方法就会被调用,之后会就进入到这个拦截器的intercept方法逻辑中 public boolean isMatch(Method candidateMethod) { return isSetBeanFactory(candidateMethod); } public static boolean isSetBeanFactory(Method candidateMethod) { return (candidateMethod.getName().equals("setBeanFactory") && candidateMethod.getParameterCount() == 1 && BeanFactory.class == candidateMethod.getParameterTypes()[0] && BeanFactoryAware.class.isAssignableFrom(candidateMethod.getDeclaringClass())); } }

BeanMethodInterceptor

相比于上面一个拦截器,这个拦截器的逻辑就要复杂多了,我们先来看看它的执行时机,也就是 isMatch 方法

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第16张

简而言之,就是拦截 @Bean 标注的方法,知道了执行时机后,我们再来看看它的拦截逻辑,代码其实不是很长,但是理解起来确很不容易,牵涉到 AOP 以及 Bean 的创建了,不过放心,我会结合实例给你讲明白这段代码,下面我们先看源码:

public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, MethodProxy cglibMethodProxy) throws Throwable { // 之前不是给BEAN_FACTORY_FIELD这个字段赋值了BeanFactory吗,这里就是反射获取之前赋的值 ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance); // 确定Bean的名称 String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod); // Determine whether this bean is a scoped-proxy // 判断这个Bean是否是一个域代理的类 Scope scope = AnnotatedElementUtils.findMergedAnnotation(beanMethod, Scope.class); // 存在@Scope注解,并且开启了域代理模式 if (scope != null && scope.proxyMode() != ScopedProxyMode.NO) { String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName); // 域代理对象的目标对象正在被创建,什么时候会被创建?当然是使用的时候嘛 if (beanFactory.isCurrentlyInCreation(scopedBeanName)) { // 使用的时候调用@Bean方法来创建这个域代理的目标对象,所以@Bean方法代理的时候针对的是域代理的目标对象,目标对象需要通过getBean的方式创建 beanName = scopedBeanName; } } // 判断这个bean是否是一个factoryBean if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX beanName) && factoryContainsBean(beanFactory, beanName)) { Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX beanName); if (factoryBean instanceof ScopedProxyFactoryBean) { // ScopedProxyFactoryBean还记得吗?在进行域代理时使用的就是这个对象 // 对于这个FactoryBean我们是不需要进行代理的,因为这个factoryBean的getObject方法 // 只是为了得到一个类似于占位符的Bean,这个Bean只是为了让依赖它的Bean在创建的过程中不会报错 // 所以对于这个FactoryBean我们是不需要进行代理的 // 我们只需要保证这个FactoryBean所生成的代理对象的目标对象是通过getBean的方式创建的即可 } else { // 而对于普通的FactoryBean我们需要代理其getObject方法,确保getObject方法产生的Bean是通过getBean的方式创建的 // It is a candidate FactoryBean - go ahead with enhancement return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName); } } // 举个例子,假设我们被@Bean标注的是A方法,当前创建的BeanName也是a,这样就符合了这个条件 // 但是如果是这种请求,a(){b()},a方法中调用的b方法,那么此时调用b方法创建b对象时正在执行的就是a方法 // 此时就不满足这个条件,会调用这个resolveBeanReference方法来解决方法引用 if (isCurrentlyInvokedFactoryMethod(beanMethod)) { // 如果当前执行的方法就是这个被拦截的方法,(说明是在创建这个Bean的过程中) // 那么直接执行目标类中的方法,也就是我们在配置类中用@Bean标注的方法 return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs); } // 说明不是在创建中了,而是别的地方直接调用了这个方法,这时候就需要代理了,实际调用getBean方法 return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);

private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs, ConfigurableBeanFactory beanFactory, String beanName) { // 什么时候会是alreadyInCreation?就是正在创建中,当Spring完成扫描后得到了所有的BeanDefinition // 那么之后就会遍历所有的BeanDefinition,根据BeanDefinition一个个的创建Bean,在创建Bean前会将这个Bean // 标记为正在创建的,如果是正在创建的Bean,先将其标记为非正在创建,也就是这行代码beanFactory.setCurrentlyInCreation(beanName, false) // 这是因为之后又会调用getBean方法,如果已经被标记为创建中了,那么在调用getBean时会报错 boolean alreadyInCreation = beanFactory.isCurrentlyInCreation(beanName); try { // 如果是正在创建的Bean,先将其标记为非正在创建,避免后续调用getBean时报错 if (alreadyInCreation) { beanFactory.setCurrentlyInCreation(beanName, false); } // 在调用beanMthod的时候,也就是被@Bean注解标注的方法的时候如果使用了参数,只要有一个参数为null,就直接调用getBean(beanName),否则带参数调用getBean(beanName,args),后面通过例子解释这段代码 boolean useArgs = !ObjectUtils.isEmpty(beanMethodArgs); if (useArgs && beanFactory.isSingleton(beanName)) { for (Object arg : beanMethodArgs) { if (arg == null) { useArgs = false; break; } } } Object beanInstance = (useArgs ? beanFactory.getBean(beanName, beanMethodArgs) : beanFactory.getBean(beanName)); // 这里发现getBean返回的类型不是我们方法返回的类型,这意味着什么呢? // 在《你知道Spring是怎么解析配置类的吗?》我有提到过BeanDefinition的覆盖 // 这个地方说明beanMethod所定义的bd被覆盖了 if (!ClassUtils.isAssignableValue(beanMethod.getReturnType(), beanInstance)) { if (beanInstance.equals(null)) { beanInstance = null; } else { // 省略日志 throw new IllegalStateException(msg); } } // 注册Bean之间的依赖关系 // 这个method是当前执行的一个创建bean的方法 Method currentlyInvoked = SimpleInstantiationStrategy.getCurrentlyInvokedFactoryMethod(); // 不等于null意味着currentlyInvoked这个方法创建的bean依赖了beanName所代表的Bean // 在开头的例子中,currentlyInvoked就是a(),beanName就是dmzService,outBeanName就是a if (currentlyInvoked != null) { String outerBeanName = BeanAnnotationHelper.determineBeanNafanhr(currentlyInvoked); // 注册的就是a跟dmzService的依赖关系,注册到容器中的dependentBeanMap中 // key为依赖,value为依赖所在的bean beanFactory.registerDependentBean(beanName, outerBeanName); } return beanInstance; } finally { if (alreadyInCreation) { // 实际还在创建中,要走完整个生命周期流程 beanFactory.setCurrentlyInCreation(beanName, true); } } }

3、结合例子讲解难点代码

这部分内容非常细,不感兴趣可以跳过,主要是 BeanMethodInterceptor 中的方法。

3.1、判断这个Bean是否是一个域代理的类

示例代码

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第17张

我们需要调试两种情况
  • 创建 Controller 时,注入 dmzService,因为 dmzService 是一个 request 域的对象,正常情况下注入肯定是报错的,但是我们在配置类上对域对象开启了代理模式,所以在创建 Controller 时会注入一个代理对象。

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第18张

断点调试,也确实如我们所料,这个地方注入的确实是一个代理对象,因为我们在配置类上申明了proxyMode = ScopedProxyMode.TARGET_CLASS,所以这里是一个 cglib 的代理对象。

使用 dmzService 的时候,这个时候使用的应该是实际的目标对象。所以按照我们的分析应该通过getBean(targetBeanName) 的方式来获取到这个 Bean,执行流程应该是代理对象 cglibDmzService 调用了toString 方法,然后调用 getBean,getBean 要根据 BeanDefinition 创建 Bean,而根据 BeanDefinition 的定义,需要使用配置类中的 BeanMethod 来创建 Bean,所以此时会进入到 BeanMethodInterceptor的intecept方法。

我们直接在 intecept 方法中进行断点,会发现此时的调用栈如下

2w字长文给你讲透了配置类为什么要添加(configure配置类)-第19张

  • 打印时,调用了toString 方法
  • 实际将会去创建目标 Bean,所以此时 getBean 时对应的 BeanName 为targetBeanName("scopedTarget." beanName)
  • 在 getBean 时根据 BeanDefinition 的定义会通过执行配置类中的 beanMethod 方法来创建 Bean
  • 最终就进入了拦截器中这个方法
  • 这种情况下就会进入到下面这段代码的逻辑中

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第20张

    3.3、方法引用的情况下,为什么会出现 Bean 正在创建中(isCurrentlyInCreation)?

    也就是下面这段代码什么时候会成立

    if (alreadyInCreation) { beanFactory.setCurrentlyInCreation(beanName, false); }

    示例代码

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第21张

    上面这种配置,在启动的时候就会进入到 if 条件中,在创建 a 的时候发现需要注入 b,那么 Spring 此时就会去创建 b,b 在创建的过程中又调用了 a 方法,此时 a 方法在执行时又被拦截了,然后就会进入到 if 判断中去。对Spring 有一定了解的同学应该能感觉到,这个其实跟循环依赖的原理是一样的。关于循环依赖,在后面我单独写一篇文章进行说明。

    3.4、if (arg == null) {useArgs = false;}是什么意思?

    这个代码我初看时也很不明白,为什么只要有一个参数为 null 就直接标记成不使用参数呢?我说说自己的理解。

    beanMethodArgs 代表了调用 beanMethod 时传入的参数,正常 Spring 自身是不会传入这个参数的,因为没有必要,创建 Bean 时其依赖早就通过 BeanDefinition 确定了,但是可能出现下面这种情况

    示例代码

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第22张

    这种情况下,我们在orderService()为了得到当前容器中的 dmzService 调用了对应的 BeanMethod,但是按照方法的定义我们不得不传入一个参数,但是实际上我们知道 BeanMethod 等价于 getBean,所以上面这段代码可以等价于

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第23张

    对于 getBean 而言,传入参数跟不传参数在创建 Bean 时是有区别的,但是创建后从容器中获取 Bean 时跟传入的参数没有一毛钱关系(单例情况),因为这是从缓存中获取嘛。也就是说单例下,传入的参数只会影响第一次创建。正因为如此,getBean 在单纯的做获取的时候不需要参数,那就意味着 beanMthod 在获取 Bean 的时候也可以不传入参数嘛,但是 beanMthod 作为一个方法又定义了形参,Spring 就说,这种情况你就传个 null 吧,反正我知道要去 getBean,当然,这只是笔者的个人理解。

    4、结合Spring整体对ConfigurationClassEnhancer相关源码分析总结

    4.1、Bean工厂后置处理器修改bd,对应enhance方法

    执行流程

    修改bd的整个过程都发生在Bean工厂后置处理器的执行逻辑中

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第24张

    执行逻辑

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第25张

    在上文中我们已经知道了,在执行bean工厂后置处理器前,Spring容器的状态如下:

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第26张

    那么执行完成Bean工厂后置处理器后(不考虑程序员自定义的后置处理器),容器的状态应该是这样的

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第27张

    4.2、BeanFactoryAwareMethodInterceptor

    执行流程

    在容器中的 bd 就绪后,Spring 会通过 bd 来创建 Bean了,会先创建配置类,然后创建配置类中 beanMethod 定义的 bean。在创建配置类的过程中在初始化 Bean 时,如果实现了 Aware接口,会调用对于的 setXxx 方法,具体代码位于org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第28张

    在调用setBeanFactory方法时,会被拦截,进入到拦截器的逻辑中

    执行逻辑

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第29张

    4.3、BeanMethodInterceptor

    执行流程

    以下面这段代码为例:

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第30张

    Spring会根据beanMethod在配置类中定义顺序来创建Bean,所以上面这段配置会先创建dmzServcice,之后在创建orderService。

    那么BeanMethodInterceptor的拦截将会发生在两个地方

    1. 直接创建 dmzService 的过程中,拦截的是dmzService()方法
    2. 创建 orderService 过程中,第一次拦截的是orderService()方法
    3. orderService() 方法调用了dmzService()方法,dmzService() 方法又被拦截

    在直接创建dmzService时,由于isCurrentlyInvokedFactoryMethod(beanMethod)这句代码会成立,所以会直接调用目标类的方法,也就是cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs),就是我们在配置类中定义的dmzService()方法,通过这个方法返回一个dmzService

    而创建orderService时,方法的调用就略显复杂,首先它类似于上面的直接创建dmzService的流程,orderService()方法会被拦截,但是由于正在执行的方法就是orderService()方法,所以orderService()也会被直接调用。但是orderService()中又调用了dmzService()方法,dmzService()方法又被拦截了,此时orderService()还没被执行完成,也就是说正在执行的方法是orderService()方法,所以isCurrentlyInvokedFactoryMethod(beanMethod)这句代码就不成立了,那么就会进入org.springframework.context.annotation.ConfigurationClassEnhancer.BeanMethodInterceptor#resolveBeanReference这个方法的逻辑中,在这个方法中,最终又通过getBean方法来获取dmzService,因为dmzService之前已经被创建过了,所以在单例模式下,就直接从单例池中返回了,而不会再次调用我们在配置类中定义的dmzService()方法。

    执行逻辑

    2w字长文给你讲透了配置类为什么要添加(configure配置类)-第31张

    对了,在这里说一下,我目前是在职Java开发,如果你现在正在学习Java,了解Java,渴望成为一名合格的Java开发工程师,在入门学习Java的过程当中缺乏基础入门的视频教程,可以关注并私信我:01。获取。我这里有最新的Java基础全套视频教程。

    ,

    版权声明:该问答观点仅代表作者本人。如有侵犯您版权权利请告知 cpumjj@hotmail.com,我们将尽快删除相关内容。