SpringBoot自定义自动装配与Conditional失效问题

前言

参考书籍《SpringBoot编程思想》— 小马哥mercyblitz

此书是难得的讲述SpringBoot的一本好书,由Spring的注解发展史介绍到Spring的注解驱动,以一个合适的切入点展开对SpringBoot注解驱动的加载和SpringApplication的启动过程的讨论。

建议有Spring基础再去看此书,收益颇丰。

本篇文章是上一篇文章 SpringBoot自动装配魔法之源码解析 的番外篇,主要的议题有下面两点:

  • 示范一个专业的自动装配starter应该是怎样的,以及如何进行自定义的自动装配
  • @ConditionalOnBean注解失效问题

自动装配规则

类命名规则

从spring.factories文件中,以EnableAutoConfiguration为key来搜索
在这里插入图片描述
可以发现一个规律,其自动装配的Bean的名称都是以AutoConfiguration结尾的,所以这里我们可以知道,类名需要以AutoConfiguration结尾。

package命名规则

还是以上述的类作为例子,我们随机截取三个类的包名作为示范:

  • org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
  • org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
  • org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration

可以发现,他们都是以org.springframework.boot.autoconfigure为开头的,org.springframework.boot说明这些都是官方的自动装配,而autoconfigure包说明用来存放自动装配类的。

从这里我们可以发现,命名的规则就是

${com.xxx.xxx}.autoconfigure.${功能模块名,如aop}.*AutoConfiguration

jar包构建规则

jar包结构

在官方文档中建议分为两个jar包,一个autoconfigure包,存放自动装配类和spring.factories,一个starter包,用来maven依赖刚刚的autoconfigure。就像下面这样
在这里插入图片描述
而starter单独一个jar包,依赖于上面的包。
在这里插入图片描述
在官方文档中说到,建议这样做,但如果需要简单的话,合并成一个jar包也是可以的。

jar包取名

接下来就是给jar包取名字了,在官方文档中,推荐开发人员使用如下命名

${module}-spring-boot-starter

此模式属于“第三方自定义starter”,而官方stater是什么样子呢?

spring-boot-starter-${module}

区别就在模块名在前在后,starter在前则表示此starter为官方定义的。从上面图片也可以看出这一点。

构建自定义的自动装配

接下来就开始自定义一个自动装配jar了。首先构建一个工程,其工程名为

<artifactId>stringbean-spring-boot-starter</artifactId>

然后创建一个合适的包名
在这里插入图片描述
构建一个自动装配的配置类

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

将以上配置类放入META-INF下的spring.factories文件中去

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.microservice.original.autoconfigure.springbean.StringBeanAutoConfiguration

这样一个stater就做好了。然后将其jar依赖添加到另一个工程的pom文件中去

<dependency>
  <groupId>com.microservice.original</groupId>
  <artifactId>stringbean-spring-boot-starter</artifactId>
  <version>0.0.1-SNAPSHOT</version>
</dependency>

测试的工程基本没东西
在这里插入图片描述
编写引导类

@EnableAutoConfiguration
public class TestAutoConfigure {

  public static void main(String[] args) {
    ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfigure.class)
      // 非WEB
      .web(WebApplicationType.NONE)
      .run(args);

    // 获取上下文中,名为stringBean的Bean,类类型为String
    String stringBean = context.getBean("stringBean", String.class);
    System.out.println(stringBean);
    context.close();
  }
}

控制台打印
在这里插入图片描述
这样,一个自定义的自动装配就完成了。

但其实到这里还不够专业,你还需要例如条件前置过滤,分析在什么时候自动装配,在什么时候不自动装配,并不是引入jar包就自动装配上去。关于条件的配置可以配置在spring-autoconfigure-metadata.properties文件中。关于前置filter过滤在讲解自动装配的魔法的那篇文章有深入源码分析的过程。

条件前置过滤其实也只是粗略过滤一下,实质上详细的过滤,你需要在自动装配的配置Bean中打上各种条件过滤注解,例如:

  • @ConditionalOnBean:当存在某Bean时进行装配Bean
  • @ConditionalOnClass:当存在某Class时进行装配Bean
  • 以上注解均存在相反注解,例如@ConditionalOnMissingClass:当不存在某Class时进行装配Bean
  • 还有很多@Conditionalxxx注解,当然你也可以自定义

总之,一个合格的条件过滤,是一个专业的自动装配Bean必不可少的。

@ConditionOnBean失效问题

首先,我们先来看一个示例。自定义一个配置类在包下

@Configuration
public class TestConfiguration {
    @Bean
    @ConditionalOnBean(User.class)
    public Test test() {
        return new Test();
    }
}

其含义是,在上下文中若有User这个对象的Bean,则装配Test对象,然后在引导类配置User这个Bean

@EnableAutoConfiguration
@ComponentScan
public class TestAutoConfigure {

  public static void main(String[] args) {

    ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfigure.class)
      // 非WEB
      .web(WebApplicationType.NONE)
      .run(args);

    System.out.println("是否有名为user这个Bean: " + context.containsBean("user"));
    System.out.println("其类型为: " + context.getBean("user"));

    System.out.println("是否有名为test这个Bean: " + context.containsBean("test"));
    context.close();
  }

  @Bean
  public User user(){
    return new User("xx");
  }
}

控制台打印
在这里插入图片描述
这里不禁发起疑问,为什么明明有User这个Bean,Test却没有被装配进来呢?我们这里注释掉Test的Conditional注解

@Bean
//@ConditionalOnBean(User.class)
public Test test() {
  return new Test();
}

再次运行,查看控制台
在这里插入图片描述
这个配置类确实有作用,所以问题就出在@ConditionalOnBean注解上。

其实,@ConditionalOnBean这个注解是给自动装配的配置类使用的,而不是自定义的配置类。

由于此注解的特殊性,其检查的是上下文中的Bean,而这就依赖于Bean的注册顺序。如果检查时机过早,导致了检查的时候,你需要判断的Bean都还没注册到Spring上下文中,这就失去了此注解需要有的意思。

如果我们将@ConditionalOnBean判断移到自动装配的配置Bean上呢?

将我们的自动装配Bean调整如下

@Configuration
public class StringBeanAutoConfiguration {
  @Bean
  @ConditionalOnMissingBean(name = "user")
  public String stringBean(){
    return "world,hello";
  }
}

当上下文中不存在名为user的Bean时才进行装配。然后引导类如下

@EnableAutoConfiguration
@ComponentScan
public class TestAutoConfigure {

  public static void main(String[] args) {

    ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfigure.class)
      // 非WEB
      .web(WebApplicationType.NONE)
      .run(args);

    System.out.println("是否有名为user这个Bean: " + context.containsBean("user"));
    System.out.println("是否有名为stringBean这个Bean: " + context.containsBean("stringBean"));
    context.close();
  }

  @Bean
  public User user(){
    return new User("xx");
  }
}

此时是有user这个Bean的,控制台打印
在这里插入图片描述
如果把user这个Bean注释掉呢?
在这里插入图片描述
此时的结果是符合预期的。

为何非自动装配的配置会失效?

因为在解析配置的时候是有一个顺序的,若阅读过源码就可以知道,扫描到的Bean的顺序会比较提前一点处理,假设我这边有一个引导类,一个配置类
在这里插入图片描述
而注册到IOC容器中的顺序如下
在这里插入图片描述
此时处理代码坐标为ConfigurationClassProcessor处理类的processConfigBeanDefinitions方法中

// 这里parser.getConfigurationClasses()得到的集合就是上述图片那个集合
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());

// Read the model and create bean definitions based on its content
if (this.reader == null) {
  this.reader = new ConfigurationClassBeanDefinitionReader(
    registry, this.sourceExtractor, this.resourceLoader, this.environment,
    this.importBeanNameGenerator, parser.getImportRegistry());
}
// 注册解析到的类
this.reader.loadBeanDefinitions(configClasses);
alreadyParsed.addAll(configClasses);

也就是说,注册顺序就是上述那个顺序,我们定义的配置类将首先注册到Spring上下文,其定义的@ConditionalOnBean注解的属性值此时是第二位解析的(在引导类中),所以此时的Conditional条件就不匹配了,因为你的条件Bean都还没注册到上下文呢。为了验证这个想法,我们将conditional注解移到引导类上,引导类是比配置类晚注册的,照理来说它的条件是可以匹配到的。

@EnableAutoConfiguration
@ComponentScan
public class TestAutoConfigure {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfigure.class)
                // 非WEB
                .web(WebApplicationType.NONE)
                .run(args);

        System.out.println("是否有名为user这个Bean: " + context.containsBean("user"));
        System.out.println("是否有名为test这个Bean: " + context.containsBean("test"));
        context.close();
    }

    @Bean
    @ConditionalOnBean(Test.class)
    public User user(){
        return new User("xx");
    }
}

而我们的配置类如下

@Configuration
public class TestConfiguration {
    @Bean
    public Test test() {
        return new Test();
    }
}

运行引导类,控制台打印
在这里插入图片描述
结果符合预期,@ConditionalOnBean没有失效了。

这里我举这个例子是为了说明配置顺序决定了是否失效,并不是在提供一个解决方案。大家在平时的配置类中最好不要用@ConditionalOnBean注解,此注解是给自动装配的情况用是比较合适的。因为在平时的配置类中,顺序是不能确定的,此顺序还依赖扫描的顺序,文件存放的顺序,加载方式的顺序,具有很大的不确定性。

为何自动装配的配置就有效?

回顾一下上面的那个集合的图,可以看到,所有自动装配的Bean都是在末尾处,它们的顺序是得到保障的,所以@ConditionalOnBean注解可以正常使用。

那么为什么自动装配的Bean一定是在集合的末尾处呢?由 自动装配的魔法 文章中讲解的自动装配的原理可以得知,其核心是由@Import注解实现的

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

而AutoConfigurationImportSelector这个类结构如下所示
在这里插入图片描述
可见,它是一个DeferredImportSelector,延迟性的导入特性,正如讲解自动装配的那篇文章中说到的,其解析处理是比普通的Bean都晚

public void parse(Set<BeanDefinitionHolder> configCandidates) {
  // 循环解析普通的Bean
  for (BeanDefinitionHolder holder : configCandidates) {
    BeanDefinition bd = holder.getBeanDefinition();
    parse(bd.getBeanClassName(), holder.getBeanName());
  }  

  // 处理延迟Import的Bean
  this.deferredImportSelectorHandler.process();
}

在解析的方法中就可以看出此时机,是最晚处理的,所以其在集合列表中处于末尾位置,在注册自动装配的Bean时,判断Bean是否存在的时候就已经把该注册的Bean都注册上了,此时的Bean判断才是合理的。

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

猜你喜欢

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