赞
踩
参考书籍《SpringBoot编程思想》— 小马哥mercyblitz
此书是难得的讲述SpringBoot的一本好书,由Spring的注解发展史介绍到Spring的注解驱动,以一个合适的切入点展开对SpringBoot注解驱动的加载和SpringApplication的启动过程的讨论。
建议有Spring基础再去看此书,收益颇丰。
本篇文章是上一篇文章 SpringBoot自动装配魔法之源码解析 的番外篇,主要的议题有下面两点:
从spring.factories文件中,以EnableAutoConfiguration为key来搜索
可以发现一个规律,其自动装配的Bean的名称都是以AutoConfiguration结尾的,所以这里我们可以知道,类名需要以AutoConfiguration结尾。
还是以上述的类作为例子,我们随机截取三个类的包名作为示范:
可以发现,他们都是以org.springframework.boot.autoconfigure为开头的,org.springframework.boot说明这些都是官方的自动装配,而autoconfigure包说明用来存放自动装配类的。
从这里我们可以发现,命名的规则就是
${com.xxx.xxx}.autoconfigure.${功能模块名,如aop}.*AutoConfiguration
在官方文档中建议分为两个jar包,一个autoconfigure包,存放自动装配类和spring.factories,一个starter包,用来maven依赖刚刚的autoconfigure。就像下面这样
而starter单独一个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中打上各种条件过滤注解,例如:
总之,一个合格的条件过滤,是一个专业的自动装配Bean必不可少的。
首先,我们先来看一个示例。自定义一个配置类在包下
@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判断才是合理的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。