SpringBoot的自动装配原理解析(第二章)之 通过@ConditionalOnBean,@ConditionalOnXXX注解 实现Bean按需加载

568 阅读5分钟

写在前面

今天继续聊聊SpringBoot的自动装配原理,有兴趣的靓仔可以看看第一篇入门文章

SpringBoot的自动装配原理解析(第一章)之 通过spring.factories文件跨模块实例化

在这个流程之上,我们先来引入两个问题,由问题入手来看自动装配

1 我们在引入第三方jar包,例如公司统一架构包时,一般包内都会封装了统一异常处理,统一权限拦截处理,
但是有时候对于我们的业务来说,某些功能是需要单独定制的,例如
三方Jar内有统一权限拦截,处理所有拦截请求,但是业务想开放黑白名单,业务自行处理
三方Jar内有统一异常处理,例如对500的报错,请联系服务员;业务想改成 请联系负责人 等等
也就是我们并不想使三方Jar的某些Bean生效
这个时候我们一般会在启动类这样操作,也就是移除这个Bean的加载,那么这个配置是怎么生效的?怎么被移除的?
@SpringBootApplication(exclude = BaseInterpreterAutoConfiguration.class)

2 第一篇文章我们阐述了spring.factories文件作用是跨模块实例化,那么有一个问题,
  假设我的spring.factories 配置了1000Bean,那么对于这1000Bean,Spring都有必要去全部加载吗?或者说我如何按需加载?例如
  某三方Jar包封装了ES,Redis,Mongo,Mysql的操作方法,其对应的spring.factories自然也有ES,Redis,Mongo,Mysql的初始Bean配置;
  但是对于我们业务来说,我只需要操作Redis即可,不需要其他花里胡哨的,也就是Spring只需要加载Redis相关的Bean即可,不需要去加载ES,Mongo,MysqlBean
  
  所以我们去看文档时,一般是这样写的,如果你需要使用Redis,你只需要在启动类加上@EnableRedis即可,如果你需要使用Mongo,你只需要在启动类上加载@EnableMongo即可
  然后我们再看内部的源码,会发现@ConditionalOnBean这个注解,
  所以是否@ConditionalOnBean这个类,能让你实现按需加载呢? 或者其作用是什,有类似的其他注解吗?
  
 @ConditionalOnBean(annotation = EnableAuthAutoConfiguration.class)
 public class AuthAutoConfiguration {}

@SpringBootApplication(exclude = *.class) 生效机制

  直接进入核心源码
  AutoConfigurationImportSelector.selectImports()中
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
   try {
      AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
            .loadMetadata(this.beanClassLoader);
      AnnotationAttributes attributes = getAttributes(annotationMetadata);
      // 装载spring.factories配置 
      List<String> configurations = getCandidateConfigurations(annotationMetadata,
            attributes);
      configurations = removeDuplicates(configurations);
      configurations = sort(configurations, autoConfigurationMetadata);
      // 获取启动类注解上面 需要移除Bean加载的信息配置
      Set<String> exclusions = getExclusions(annotationMetadata, attributes);
      checkExcludedClasses(configurations, exclusions);
      // 移除Bean
      configurations.removeAll(exclusions);
      configurations = filter(configurations, autoConfigurationMetadata);
      fireAutoConfigurationImportEvents(configurations, exclusions);
      return configurations.toArray(new String[configurations.size()]);
   }
   catch (IOException ex) {
      throw new IllegalStateException(ex);
   }
}

wecom-temp-1c5a5ed8ca86f2fd9fc178322b6337e3.png

@ConditionOnBean作用

简单理解就是当Spring容器中存在指定class实例的对象时,对应的配置才生效
伪代码举例: 
假设我们自己搞个starter Jar包给业务方提供Redis服务,
要求业务必须在启动类上加上@EnableRedis,这样才表示开启提供Redis服务
也就是启动类加上了@EnbaleRedis 才会去装载Redis相关的Bean配置
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableRedis {

}
//启动类
@EnableRedis
public class SpringEasyMain {
    public static void main(String[] args) {
        SpringApplication.run(SpringEasyMain.class);
    }
}

@ConditionalOnBean(annotation = EnableRedis.class)
@Bean
public ConditionalOnExpressionDemo getConditionalOnExpressionDemo() {
    System.out.println("验证启动类加上了@EnableRedis,才会去装载Redis相关的Bean配置");
    return ConditionalOnExpressionDemo.builder().name("test").age(20).build();
}

wecom-temp-a7e1b8288877c079115411a014aa4550.png

类似的还有
@ConditionalOnExpression(value = "1<11")当表达式 成立,配置生效
伪代码
@ConditionalOnExpression(value = "1<11")
@Bean
public ConditionalOnExpressionDemo getConditionalOnExpressionDemo() {
    System.out.println(" 验证 当表达式为true的时候,才会实例化一个Bean 1 ");
    return ConditionalOnExpressionDemo.builder().name("test").age(20).build();
}

@ConditionalOnExpression(value = "50>10")
@Bean
public ConditionalOnExpressionDemo getConditionalOnExpressionDemo2() {
    System.out.println(" 验证 当表达式为true的时候,才会实例化一个Bean 2");
    return ConditionalOnExpressionDemo.builder().name("test").age(20).build();
}

企业微信截图_c61e20cc-52ec-4e34-9188-71064d254ebd.png

@ConditionOn***这种的作用简单讲就是 满足***条件时,才会去装载配置****
这部分我也写了部分demo,有兴趣的靓仔可以直接下载验证

也就是总结一下: 我们用@ConditionOnBean这种操作, 可以实现Bean的按需加载,条件加载
我们用在使用某三方包时,
@EnableRedis 表示开启Redis功能,
@EnableEs   表示开启Es功能
从而实现动态的Bean加载
当然还有很多花里胡哨的类似@Condition***,有兴趣自行去度娘了

wecom-temp-b1935ffe8b66d0bf7781a44068abb232.png

@ConditionalOn注解 Demo 下载

@ConditionOnBean源码解析

直接上干货,进入核心源码,入口依旧是 
AutoConfigurationImportSelector.selectImports()中,这个方法结束后,我们确实获取到了当前spring.factories中的Bean 配置,并且也筛选出去了@SpringBootApplication(exclude = *.class)这个启动类配置的Bean,
那么@ConditionOnBean 这种条件筛选时怎么做的?
来看源码,往下走ConfigurationClassParser

wecom-temp-904a5b25c9ac3bde99c20b2ea5c8594c.png

企业微信截图_792653aa-838a-4210-a563-bdeddfd00ac9.png

processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);方法进去
找到processConfigurationClass(candidate.asConfigClass(configClass));

wecom-temp-3ca9edc6a57adf1b45c5367150328763.png

继续往下,直到this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)核心源码,进行@Condition** 适配

wecom-temp-4c4d4392a752ffaf2c2ed50db3ad489d.png

直接Debug到核心匹配源码SpringBootCondition.matches()

wecom-temp-e39b8f17b6533d4ab12d8a19753c99de.png

有兴趣的同学可以继续往下,这块方法即是@Condition**的判断逻辑
public final boolean matches(ConditionContext context,
      AnnotatedTypeMetadata metadata) {
   String classOrMethodName = getClassOrMethodName(metadata);
   try {
      ConditionOutcome outcome = getMatchOutcome(context, metadata);
      logOutcome(classOrMethodName, outcome);
      recordEvaluation(context, classOrMethodName, outcome);
      return outcome.isMatch();
   }
当然作者之前也看过一篇文章,描述的是@Condition**,匹配验证的时机在于AutoConfigurationImportSelector.selectImports()这个入口方法中,也就是我圈中的地方;
但是我在debug调试时,得出的结论确实不一致的,这块目前还存疑,如果有靓仔高见,还请不吝赐教

wecom-temp-1bbffd93faa59ad7763c81b419b58c3e.png

文章总结

在上一篇讲到SpringBoot自动装配时,我们留下了两个问题;
一是SpringBoot如何排除不需要的Bean,直接使用@SpringBootApplication(exclude = BaseInterpreterAutoConfiguration.class)这种配置接口;
二是spring.factories的配置的Bean如何进行按需装载,需要使用到@ConditionalOn**等注解操作,进行条件筛选
有了spring.factories文件,以及@ConditionalOn**注解,SpringBoot自动装配的原理自然就明了了

未来可期

自动装配的原理是懂了,接下来可以搞一手实战,例如可以自己动手写一个中间件starter,

就比如 springboot-mymq-starter,面向公司各业务提供数据服务,开箱即用,麻瓜式API服务

你就告诉他,你只需要引入我的pom包,加个@EnableMq注解就可以操作mq了,什么发送消息,消息重试,死信我都给你搞好了,支持QPS 2W+,放心大胆的用,不要再造轮子

当然这也是下期文章的内容 SpringBoot的自动装配原理解析(第三章)之 教你手写中间件springboot-demo-stater