写在前面
今天继续聊聊SpringBoot的自动装配原理,有兴趣的靓仔可以看看第一篇入门文章
SpringBoot的自动装配原理解析(第一章)之 通过spring.factories文件跨模块实例化
在这个流程之上,我们先来引入两个问题,由问题入手来看自动装配
1 我们在引入第三方jar包,例如公司统一架构包时,一般包内都会封装了统一异常处理,统一权限拦截处理,
但是有时候对于我们的业务来说,某些功能是需要单独定制的,例如
三方Jar内有统一权限拦截,处理所有拦截请求,但是业务想开放黑白名单,业务自行处理
三方Jar内有统一异常处理,例如对500的报错,请联系服务员;业务想改成 请联系负责人 等等
也就是我们并不想使三方Jar的某些Bean生效
这个时候我们一般会在启动类这样操作,也就是移除这个Bean的加载,那么这个配置是怎么生效的?怎么被移除的?
@SpringBootApplication(exclude = BaseInterpreterAutoConfiguration.class)
2 第一篇文章我们阐述了spring.factories文件作用是跨模块实例化,那么有一个问题,
假设我的spring.factories 配置了1000个Bean,那么对于这1000Bean,Spring都有必要去全部加载吗?或者说我如何按需加载?例如
某三方Jar包封装了ES,Redis,Mongo,Mysql的操作方法,其对应的spring.factories自然也有ES,Redis,Mongo,Mysql的初始Bean配置;
但是对于我们业务来说,我只需要操作Redis即可,不需要其他花里胡哨的,也就是Spring只需要加载Redis相关的Bean即可,不需要去加载ES,Mongo,Mysql的Bean
所以我们去看文档时,一般是这样写的,如果你需要使用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);
}
}
@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();
}
类似的还有
@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();
}
@ConditionOn***这种的作用简单讲就是 满足***条件时,才会去装载配置****
这部分我也写了部分demo,有兴趣的靓仔可以直接下载验证
也就是总结一下: 我们用@ConditionOnBean这种操作, 可以实现Bean的按需加载,条件加载
我们用在使用某三方包时,
@EnableRedis 表示开启Redis功能,
@EnableEs 表示开启Es功能
从而实现动态的Bean加载
当然还有很多花里胡哨的类似@Condition***,有兴趣自行去度娘了
@ConditionOnBean源码解析
直接上干货,进入核心源码,入口依旧是
AutoConfigurationImportSelector.selectImports()中,这个方法结束后,我们确实获取到了当前spring.factories中的Bean 配置,并且也筛选出去了@SpringBootApplication(exclude = *.class)这个启动类配置的Bean,
那么@ConditionOnBean 这种条件筛选时怎么做的?
来看源码,往下走ConfigurationClassParser
processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);方法进去
找到processConfigurationClass(candidate.asConfigClass(configClass));
继续往下,直到this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)核心源码,进行@Condition** 适配
直接Debug到核心匹配源码SpringBootCondition.matches()
有兴趣的同学可以继续往下,这块方法即是@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调试时,得出的结论确实不一致的,这块目前还存疑,如果有靓仔高见,还请不吝赐教
文章总结
在上一篇讲到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