Spring手动挡装配Bean之Enable模式

前言

在SpringBoot开发中,我们经常见到Enable的模式开发,例如在SpringCloud中需要使用feign,此时需要开启Feign的注册,打上@EnableFeignClients注解,即可扫描带@FeignClient注解的FeignClient。又或是开启异步的功能,你需要加上@EnableAsyn注解。这种模式被称为“Enable模式”,也被视作SpringBoot中的“手动挡式自动装配”。本篇文章将解析Enable模式是什么,且分析源码了解底层操作。

同时,本篇文章也是下一篇介绍SpringBoot"自动档式自动装配"的魔法的铺垫式文章,为什么这么说呢?因为在自动挡中,@EnableAutoConfiguration注解起到了关键作用,同时它也是Enable驱动模式。或许读者对此注解感到陌生,但对于@SpringBootApplication这个注解一定不会陌生,因为在SpringBoot引导类上一般都会打上、这么一个注解,而@EnableAutoConfiguration元标注了@SpringBootApplication

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  // ...
}

在Spring式的注解中,注解是具有"派生性"的,比如日常开发经常用到的一些声明Bean的注解:

  • @Service
  • @Controller
  • @Configuration
  • @Component

他们均被@Component元标注,例如@Service:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
  // ...
}

而Spring通常解析注解时,会将注解递归解析出来,并放入Map中,也就是说,会解析到最后一层,在判断是否需要视为Bean注册到IOC容器中的时候,只需要判断是否有@Component注解即可,所以不管是@Service还是@Controller,都视为@Component,将其视作SpringBean注册到IOC容器中。

需要注意的是,在Spring4才支持递归的方式解析注解,也就是不管注解有多少层,最终都能识别到最底层的关键性注解例如@Component,SpringBoot1.0开始就使用了Spring4,所以可以放心其“派生性”。但在Spring3,派生性只支持一层,如果我自定义一个注解,元标注了@Service,那么到达关键注解@Component时需要2层解析,Spring3只解析一层,并没有递归解析,此时就做不到派生性了。

在Spring中,注解都有它自己的一套方式,比如“派生性”,除此之外还有属性覆盖性,还有属性别名等等一系列的Spring专有注解模式。

综上所述,理解@Enable驱动模式,对于理解SpringBoot自动装配是必不可少的。

@Enable驱动模式

在@Enable驱动模式的开发中,可以发现,其都被@Import注解所元标注,例如@EnableFeignClients

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
  // ...
}

而Import注解中的value,一般而言都会实现ImportBeanDefinitionRegistrar或ImportSelector这两个接口,又或者是类头被@Configuration注解标注,那么从这点来看,Enable模式大致分为两种方式:

  • 注解驱动:在类中有配置注解,如@Configuration、@Bean等等
  • 接口编程:实现ImportBeanDefinitionRegistrar或ImportSelector这两个接口

然后将以上类放入@Import的value中即可,再给其加上一个华丽的外表,Enable*,这样一个Enable模式就完成了。

自定义Enable模式

下面我们自定义一个Enable模块。

注解驱动

@Configuration方式

先来一个最简单的注解驱动。首先自定义一个Enable注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(StringBean.class)
public @interface EnableStringBean {
}

其中Import中定义的配置类如下

@Configuration
public class StringBean {
    @Bean
    public String stringBean(){
        return "Hello,world";
    }
}

其实这里StringBean配置类也可以不标注@Configuration注解,在Spring3.0中限制只解析@Configuration,在后面的版本中,只要类中有@Bean、@ComponentScan等等配置注解,都可以被解析处理

@Configuration
@EnableStringBean
public class StringBeanContextBootstrap {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(StringBeanContextBootstrap.class);

        String string = context.getBean("stringBean", String.class);
        System.out.println(string);
        context.close();
    }
}

在需要装配的类上打上我们的自定义注解@EnableStringBean,装配到上下文中,控制台打印
在这里插入图片描述
可见,我们的Enable模块自动注册了一个Bean到上下文中

接口编程

ImportSelector接口方式

这种方式需要实现以上接口,并实现其selectImports方法。该方法返回一个字符串数组,这些字符串是需要被注册到Spring中的Bean的全限定类名。

这次我们来个更加动态的方式,首先重新定义Enable注解的属性

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(StringBeanRegister.class)
public @interface EnableStringBean {
    StringBean.StringType type();
}

新增一个type属性,我们可以选择性的装载某个Bean。接下来是import中的StringBeanRegister定义

public class StringBeanRegister implements ImportSelector {
  @Override
  public String[] selectImports(AnnotationMetadata importingClassMetadata) {

    // 获取EnableStringBean注解上的type属性
    Map<String, Object> attributes =
      importingClassMetadata.getAnnotationAttributes(EnableStringBean.class.getName());

    StringBean.StringType type = (StringBean.StringType) attributes.get("type");

    switch (type) {
      case HELLO_WORLD:
        return new String[]{"com.mytest.condition.config.HelloWorldBean"};
      case WORLD_HELLO:
        return new String[]{"com.mytest.condition.config.WorldHelloBean"};
      default:
        return null;
    }
  }
}

根据type值,动态判断,接下来看看我们动态判断的两个类定义和共同的接口StringBean

public class HelloWorldBean implements StringBean {
  @Override
  public String getString() {
    return "hello,world";
  }
}
public class WorldHelloBean implements StringBean {
  @Override
  public String getString() {
    return "world,hello";
  }
}
public interface StringBean {
  String getString();

  enum StringType{
    HELLO_WORLD,
    WORLD_HELLO
  }
}

很简单,我们写一个测试类来试验一下

@Configuration
@EnableStringBean(type = StringBean.StringType.WORLD_HELLO)
public class StringBeanContextBootstrap {
  public static void main(String[] args) {
    AnnotationConfigApplicationContext context =
      new AnnotationConfigApplicationContext(StringBeanContextBootstrap.class);

    StringBean string = context.getBean(StringBean.class);

    System.out.println(string.getString());

    context.close();
  }
}

这里我们的EnableStringBean注解value=StringType.WORLD_HELLO
在这里插入图片描述
可见,反向输出helloWorld的Bean被成功装载。装载方式随着type的不同而装载不同的Bean

ImportBeanDefinitionRegistrar接口方式

这种实现方式稍稍增加了难度。因为其接口方法需要自行构造BeanDefinition,并注册到Spring中,不像上面两种方式,只需要提供要注册的Bean信息。这种导入方式可以在接口方法中自行控制注册流程。

首先我们定义一个ImportBeanDefinitionRegistrar接口的实现类

public class StringBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

    StringBeanRegister register = new StringBeanRegister();
    // 使用上一个例子的代码,选择出要注册的Bean名称的全限定类名
    String[] beanName = register.selectImports(importingClassMetadata);

    Stream.of(beanName)
      // 将Stream变为BeanDefinitionBuilder集合
      .map(BeanDefinitionBuilder::genericBeanDefinition)
      // 将Stream变为BeanDefinition集合
      .map(BeanDefinitionBuilder::getBeanDefinition)
      // 将每个BeanDefinition注册到IOC中
      .forEach(beanDefinition ->
               BeanDefinitionReaderUtils.registerWithGeneratedName(beanDefinition, registry));
  }
}

这里我们看到,不仅需要提供要注册的Bean,还需要自行实现注册。Spring此种设计,无谓是拓宽了扩展性,因为有些装配只需要提供要注册的Bean类名是哪些就好了,但有些装配,需要细节到,动态自定义Bean的更多信息例如构造器参数是哪些、注册进去的BeanName需要叫什么等等,多了一个可以自定义BeanDefinition信息的维度,但有些装配不需要这么复杂。所以分为简、中、难三种维度供装配Bean的扩展,此种设计令我佩服。

手动挡装配原理

ConfigurationClassPostProcessor的注册

从这一节开始,将深入源码分析以上Enable模式的原理。

到现在我们已知@Enable*注解被@Import注解所元标注,所以我们的重点就在Spring上下文是什么时候扫描解析@Import这个注解的呢?

说到这里不得不提负责解析@Configuration的一个处理类ConfigurationClassPostProcessor,这个类不仅解析了@Import注解,还负责解析@Configuration注解。

ConfigurationClassPostProcessor是一个BeanFactoryPostProcessor,所以它具有在Spring实例化Bean之前,修改和添加Bean定义的能力。它解析大多数配置类注解,所以具有动态根据注解注册Bean到上下文中的能力。

那么是谁负责注册ConfigurationClassPostProcessor 到Spring上下文的呢?全局搜索这个类,我们发现在AnnotationConfigUtils这个类的registerAnnotationConfigProcessors方法中,注册了这个类成为SpringBean

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
  BeanDefinitionRegistry registry, @Nullable Object source) {

  // ...
  
  if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
    // 将ConfigurationClassPostProcessor类构造为BeanDefinition
    RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
    def.setSource(source);
    // 注册到IOC中
    beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
  }
  
	// ...
}

那么这个工具类的这个注册方法又被哪里引用了呢?可以找到,这个工具类将被处理注解的上下文 AnnotationConfigApplicationContext 的构造函数中调用

public AnnotationConfigApplicationContext() {
  // 将在以下AnnotatedBeanDefinitionReader类的构造函数中被构造
  this.reader = new AnnotatedBeanDefinitionReader(this);
  this.scanner = new ClassPathBeanDefinitionScanner(this);
}
public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
  // ...
  // 调用了上面说到的工具,注册了ConfigurationClassPostProcessor
  AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}

到此,就是Spring将ConfigurationClassPostProcessor这个处理类注册为SpringBean的过程。

解析@Import

在这篇文章中,不对ConfigurationClassPostProcessor进行过多的分析,只分析解析@Import注解的流程。

在postProcessBeanDefinitionRegistry方法中可以看到,由此开始了对配置注解的解析方法processConfigBeanDefinitions

public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
  List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
  // 这里获取Spring中所有的Bean名称
  String[] candidateNames = registry.getBeanDefinitionNames();

  for (String beanName : candidateNames) {
    // 转换为Bean的定义类
    BeanDefinition beanDef = registry.getBeanDefinition(beanName);
    // 判断是否已经被解析过了
    if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
        ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
      if (logger.isDebugEnabled()) {
        logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
      }
    }
    // 这里确认此Bean没有被解析过,判断是否需要被解析
    else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
      // 代码能到这里,说明此Bean需要被解析,放入集合中
      configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
    }
  }
  
  // Parse each @Configuration class
  // 解析配置注解的类
  ConfigurationClassParser parser = new ConfigurationClassParser(
    this.metadataReaderFactory, this.problemReporter, this.environment,
    this.resourceLoader, this.componentScanBeanNameGenerator, registry);

  Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
  Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
  do {
    // 解析
    parser.parse(candidates);
    
    // ...
    
    // 将解析到的Bean注册到Spring容器中去
    this.reader.loadBeanDefinitions(configClasses);
    
    // ...
}

这里值得一提的是,ConfigurationClassUtils的checkConfigurationClassCandidate方法,此方法用来判断Bean是否是一个配置类(需要被解析)

if (isFullConfigurationCandidate(metadata)) {
  beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
}
else if (isLiteConfigurationCandidate(metadata)) {
  beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
}

以上两个分支都表示此Bean需要被解析,至于Full和Lite模式,此时可以先看作一个黑盒,不作过多研究。

public static boolean isFullConfigurationCandidate(AnnotationMetadata metadata) {
   return metadata.isAnnotated(Configuration.class.getName());
}
private static final Set<String> candidateIndicators = new HashSet<>(8);

static {
  candidateIndicators.add(Component.class.getName());
  candidateIndicators.add(ComponentScan.class.getName());
  candidateIndicators.add(Import.class.getName());
  candidateIndicators.add(ImportResource.class.getName());
}

public static boolean isLiteConfigurationCandidate(AnnotationMetadata metadata) {
  // Do not consider an interface or an annotation...
  if (metadata.isInterface()) {
    return false;
  }

  // Any of the typical annotations found?
  for (String indicator : candidateIndicators) {
    if (metadata.isAnnotated(indicator)) {
      return true;
    }
  }

  // Finally, let's look for @Bean methods...
  try {
    return metadata.hasAnnotatedMethods(Bean.class.getName());
  }
  catch (Throwable ex) {
    if (logger.isDebugEnabled()) {
      logger.debug("Failed to introspect @Bean methods on class [" + metadata.getClassName() + "]: " + ex);
    }
    return false;
  }
}

上面的代码很简单,我们这里总结一下当Bean是怎样的就会去解析它呢?

只要类中具有以下注解

  • @Configuration
  • @Component
  • @ComponentScan
  • @Import
  • @ImportResource
  • @Bean

所以,只要IOC容器中有被打上@Enable*注解的Bean,此时就会被拿出来解析,执行parser的parse方法,而parser的parse方法最终会来到doProcessConfigurationClass方法,真正解析配置注解。所以对@Import注解的解析就在该方法中

// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), true);

其中,在getImports方法中,将获取@Import注解中的value值,也就是上面所说的注解编程和接口编程自定义的那个类,然后将其封装为SourceClass对象

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
                            Collection<SourceClass> importCandidates, boolean checkForCircularImports) {

  // importCandidates集合就是Import的value
  if (importCandidates.isEmpty()) {
    return;
  }

	// ...
  
  this.importStack.push(configClass);
  try {
    for (SourceClass candidate : importCandidates) {
      // 判断是否是ImportSelector的实现
      if (candidate.isAssignable(ImportSelector.class)) {
        // Candidate class is an ImportSelector -> delegate to it to determine imports
        Class<?> candidateClass = candidate.loadClass();
        // 实例化成ImportSelector类
        ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
        // 如果有aware方法,在这里会进行aware设置
        ParserStrategyUtils.invokeAwareMethods(
          selector, this.environment, this.resourceLoader, this.registry);
        // 延迟导入,其中ImportSelector接口有一个子类是DeferredImportSelector
        // 表示该Import要延迟导入,此时不做解析,只是将其保存在一个集合中去
        // 在所有配置类解析完成之后,才会对DeferredImportSelector实现类要导入的类进行解析
        if (selector instanceof DeferredImportSelector) {
          // 封装DeferredImportSelector类,将其信息保存到集合中
          this.deferredImportSelectorHandler.handle(
            configClass, (DeferredImportSelector) selector);
        }
        // 若不是延迟导入,那就直接开始导入工作
        else {
          // 调用selector的方法,获取需要被注册的Bean的类名
          String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
          Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
          // 递归解析该需要被注册的Bean
          processImports(configClass, currentSourceClass, importSourceClasses, false);
        }
      }
      // 又或者是ImportBeanDefinitionRegistrar的实现?
      else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
        // Candidate class is an ImportBeanDefinitionRegistrar ->
        // delegate to it to register additional bean definitions
        Class<?> candidateClass = candidate.loadClass();
        ImportBeanDefinitionRegistrar registrar =
          BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
        ParserStrategyUtils.invokeAwareMethods(
          registrar, this.environment, this.resourceLoader, this.registry);
        // 这里的做法是先将ImportBeanDefinitionRegistrar存放起来
        // 在稍后一点再调用其方法注册BeanDefinition
        configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
      }
      // 到这里,判断不是以上的接口编程实现
      // 或者是上面import接口编程返回出来的需要被导入的那些类,也会走到这一步
      // 将此类当作配置类解析
      else {
        // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
        // process it as an @Configuration class
        this.importStack.registerImport(
          currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
        // 接下来还会到doProcessConfigurationClass方法再进行解析
        // 还会走一样的流程进行解析配置注解
        processConfigurationClass(candidate.asConfigClass(configClass));
      }
    }
  }
  // ...
}

可以看到,这段代码的逻辑正好就是我们开头所示范的三种方式去自定义Enable模式,所以在下面就可以进行一个总结了

总结

可见,Enable模式关键其实就是@Import注解中的value所配置的类,这个类就是引导注册某些Bean的核心,到这里应该也能理解为何称之为"手动挡",因为它无法自动判断动态装配一些Bean,而是要自己去实现一个注册Bean的类,它可以是接口编程的,可以是注解驱动的,要配置什么Bean完全由手动完成,并且手动打上一个@Enable*注解。

或许这篇文章需要有ConfigurationClassPostProcessor解析逻辑基础,那么下一篇文章将开启对其的具体解析

如果有看懂的读者,就会发现,其实Import中的value所配置的类可以不必是SpringBean,例如ImportSelector接口方式,其返回的类名所对应的类可以没有任何注解,其也会被注册到Spring容器中去,亦或是注解驱动,类头也不必打上@Configuration,也可以解析@Bean、@ComponentScan等等注解,只不过如果有@Configuration注解的话此配置类是Full模式,这里也推荐打上@Configuration。关于配置模式,在具体讲解ConfigurationClassPostProcessor中也会介绍到。

在SpringBoot自动装配(自动挡)技术中,也是用到了@Enable驱动模式,但是它为什么是自动挡呢?因为它可以自动解析包下的所有@Component的类或其派生注解,将它们注册到Spring中,还能注册jar包中需要被注册的Bean,然而以上操作并不需要在你的代码中加任何代码,只需要加入对应的jar依赖,不需要其被装配也只需移除jar依赖,或是配置exclude黑名单。而开启自动挡,只需要Spring容器中有Bean被打上@EnableAutoConfiguration(派生@SpringBootApplication)即可。

发布了84 篇原创文章 · 获赞 85 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/98028560