配置类为什么要添加@Configuration注解?(2)

简介: 配置类为什么要添加@Configuration注解?(2)

2、ConfigurationClassEnhancer源码分析


2.1、创建代理过程分析


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

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

public Class<?> enhance(Class<?> configClass, @Nullable ClassLoader classLoader) {
   // 如果已经实现了EnhancedConfiguration接口,说明被代理过了,直接返回
   if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
       return configClass;
   }
   // 否则调用newEnhancer方法先创建一个增强器,然后直接使用这个增强器生成代理类的字节码对象
   Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
   if (logger.isDebugEnabled()) {
       logger.debug(String.format("Successfully enhanced %s; enhanced class name is: %s",
                                  configClass.getName(), enhancedClass.getName()));
   }
   return enhancedClass;
}
private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
   Enhancer enhancer = new Enhancer();
   // 设置目标类
   enhancer.setSuperclass(configSuperClass);
   // 让代理类实现EnhancedConfiguration接口,这个接口继承了BeanFactoryAware接口
   // 主要两个作用:1.起到标记作用,如果实现了,代表已经被代理过了
   // 2.代理类需要访问BeanFactory,所有实现了BeanFactoryAware接口
   enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
   // 设置生成的代理类不实现factory接口
   enhancer.setUseFactory(false);
   // 设置代理类名称的生成策略
   enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
   // 代理类中引入一个BeanFactory字段
   enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
   // 设置过滤器,CALLBACK_FILTER中也同时设置了拦截器
   enhancer.setCallbackFilter(CALLBACK_FILTER);
   enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
   return enhancer;
}
// 使用增强器生成代理类的字节码对象
private Class<?> createClass(Enhancer enhancer) {
   Class<?> subclass = enhancer.createClass();
   Enhancer.registerStaticCallbacks(subclass, CALLBACKS);
   return subclass;
}

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

// 声明的三个拦截器
private static final Callback[] CALLBACKS = new Callback[] {
   new BeanMethodInterceptor(),
   new BeanFactoryAwareMethodInterceptor(),
   NoOp.INSTANCE
};

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方法

public boolean isMatch(Method candidateMethod) {
   // 第一个条件,不能是Object,这个必定是满足的
   // 第二个条件,不能是setBeanFactory方法,显而易见的嘛,我们要拦截的方法实际只应该是添加了@Bean注解的方法
   // 第三个条件,添加了@Bean注解
   return (candidateMethod.getDeclaringClass() != Object.class &&
           !BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) &&
           BeanAnnotationHelper.isBeanAnnotated(candidateMethod));
}

简而言之,就是拦截@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是否是一个域代理的类


示例代码

@Configuration
@EnableAspectJAutoProxy
public class Config {
   @Bean
   @Scope(value = WebApplicationContext.SCOPE_REQUEST,proxyMode = ScopedProxyMode.TARGET_CLASS)
   public DmzService dmzService() {
       return new DmzService();
   }
}
@RestController
@RequestMapping("/test")
public class Controller {
   DmzService dmzService;
   @Autowired
   public void setDmzService(DmzService dmzService) {
       this.dmzService = dmzService;
   }
   @GetMapping("/get")
   public ResponseEntity<?> get() {
       System.out.println(dmzService);
       return ResponseEntity.ok().build();
   }
}

我们需要调试两种情况

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

微信图片_20221113155524.png

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


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

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


微信图片_20221113155555.png


  1. 打印时,调用了toString方法
  2. 实际将会去创建目标Bean,所以此时getBean时对应的BeanName为targetBeanName(“scopedTarget.”+beanName)
  3. 在getBean时根据BeanDefinition的定义会通过执行配置类中的beanMethod方法来创建Bean
  4. 最终就进入了拦截器中这个方法

这种情况下就会进入到下面这段代码的逻辑中

// 判断这个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方法代理的时候针对的是域代理的目标对象
       beanName = scopedBeanName;
   }
}

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


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

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

示例代码

@ComponentScan(value = "com.dmz.spring.first")
@Configuration
public class Config {
  @Bean
  public A a(){
    return new A();
  }
  @Bean
  public B b(){
    a();
    return new B();
  }
}
class A{
  B b;
  @Autowired
  public void setB(B b) {
    this.b = b;
  }
}
class B{
}

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


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


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


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


示例代码

@Configuration
public class AnotherConfig {
  @Bean
  public DmzService dmzService(IndexService indexService) {
    return new DmzService(indexService);
  }
  @Bean
  public OrderService orderService() {
    DmzService dmzService = dmzService(null);
    return dmzService.createOrder();
  }
}
@Component
public class IndexService {
}
public class DmzService {
  public DmzService(IndexService indexService) {
  }
  public OrderService createOrder() {
    return new OrderService();
  }
}
public class OrderService {
}

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

@Configuration
public class AnotherConfig {
  @Autowired
  ApplicationContext applicationContext;
  @Bean
  public DmzService dmzService(IndexService indexService) {
    return new DmzService(indexService);
  }
  @Bean
  public OrderService orderService() {
    DmzService dmzService = (DmzService) applicationContext.getBean("dmzService");
    return dmzService.createOrder();
  }
}

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


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


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


执行流程

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

微信图片_20221113160226.png

执行逻辑


微信图片_20221113160311.png

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

微信图片_20221113160334.png

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

微信图片_20221113160351.png


4.2、BeanFactoryAwareMethodInterceptor


执行流程

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

微信图片_20221113160438.png

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

执行逻辑

微信图片_20221113160519.png


4.3、BeanMethodInterceptor


执行流程

以下面这段代码为例:

@Configuration
public class AnotherConfig {
  @Bean
  public DmzService dmzService(){
    return new DmzService();
  }
  @Bean
  public OrderService orderService(){
    return new OrderService(dmzService());
  }
}

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()方法。


执行逻辑

微信图片_20221113160711.png


总结


这里就在上篇文章的基础上对流程图再做一次完善吧,因为图片太大了,就放个链接~

Spring创建bean前的执行流程


/

相关文章
|
2月前
|
XML Java 数据格式
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
本文介绍了如何使用Spring框架的注解方式实现AOP(面向切面编程)。当目标对象没有实现接口时,Spring会自动采用CGLIB库进行动态代理。文中详细解释了常用的AOP注解,如`@Aspect`、`@Pointcut`、`@Before`等,并提供了完整的示例代码,包括业务逻辑类`User`、配置类`SpringConfiguration`、切面类`LoggingAspect`以及测试类`TestAnnotationConfig`。通过这些示例,展示了如何在方法执行前后添加日志记录等切面逻辑。
320 2
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
|
5月前
|
XML 存储 Java
@Configuration 注解使用及源码解析
@Configuration 注解使用及源码解析
42 4
|
7月前
|
XML Java 数据格式
@Configuration配置类注解的理解
@Configuration配置类注解的理解
111 0
@Configuration配置类注解的理解
|
7月前
|
Java 数据库连接 API
SpringBoot【问题 01】借助@PostConstruct解决使用@Component注解的类用@Resource注入Mapper接口为null的问题(原因解析+解决方法)
SpringBoot【问题 01】借助@PostConstruct解决使用@Component注解的类用@Resource注入Mapper接口为null的问题(原因解析+解决方法)
799 0
记一个SpringBoot中属性注入失败的问题Consider defining a bean of type &#39;&#39;&#39; in your configuration...
记一个SpringBoot中属性注入失败的问题Consider defining a bean of type &#39;&#39;&#39; in your configuration...
301 0
|
XML Java 数据格式
【Spring注解必知必会】深度解析@Configuration注解
【Spring注解必知必会】深度解析@Configuration注解
284 0
【Spring注解必知必会】深度解析@Configuration注解
|
Java Spring 容器
配置类为什么要添加@Configuration注解?(1)
配置类为什么要添加@Configuration注解?(1)
148 0
配置类为什么要添加@Configuration注解?(1)
|
Java Spring 容器
@Configuration注解
@Configuration注解
166 0
|
容器
@Configuration和@Component注解的区别
1.@Configuration和@Component注解的源码如下 (1)Configuration注解源码如下:
198 0
记一个SpringBoot中属性注入失败的问题Consider defining a bean of type ''' in your configuration...
记一个SpringBoot中属性注入失败的问题Consider defining a bean of type ''' in your configuration...
232 0