Spring Boot以spring-boot-starter-xx命名的模块都是“开箱即用”模块,意思是说,当开发者完成依赖添加后(即pom文件的依赖添加),这个功能就会自动创建和注入到上下文中,不需要再编写麻烦的配置,只需要提供参数属性即可。十分的方便,那么如此巧妙的开箱即用是怎么实现的呢?
本文将探索其中的奥秘,笔者模拟编写自动装载JDBC的过程:
为了方便学习,将自动配置分为三部分进行理解:
业务类(待检测类)DemoJDBCService:通常使用Maven引入依赖,或Jar包等方式引入类。
自动配置类JDBCAutoConfiguration:判断业务类是否存在,若存在对业务类进行初始化、装载,比如完成读取参数、初始化等操作。
扫描加载自动配置类AutoConfigurationImportSelector:调用自动配置类。
业务类
自动配置的触发点,被检测的核心业务类,若该类存在才会进行自动配置,实现“开箱即用”的功能,否则不执行配置。
例子中,采用最简单的业务,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | public class DemoJDBCService { public String url; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public void connect(){ //链接操作 } } |
注意:这个类不能被component-scan扫描到。该bean应该由自动配置进行装载,如果被扫描到顺序不同,不符合逻辑会产生错误。(这其实是不可避免的“缺陷”,读者可以尝试在自己的项目中扫描 org.spring 会发现项目无法启动)
通俗点理解,假设本类中还依赖JDBC的相关库,如果没有加载相关库,被component-scan扫描到必然会报错NoClassDefFoundError。如果是自动装载类加载,装载类会先判断是否存在相关库,存在再加载 这时就不会报错。
自动配置类
自动配置分为三步:
(1)从application.properties中读取参数;
(2)完成对业务类Bean的初始化、装载;
(3)配置spring.factories
读取参数
方法一:
Spring 提供了一个注解用于导入配置文件中的数据 — @Value 。
1 2 | @Value("${key:value}") private String url; |
参数的key代表properties中的key,value是默认值,即若不存在该key时的取值。
注意:直接填写会当做字符串处理,如果想设null应填写#{null}
方法二:(本例采用该方法)
创建属性参数类JDBCProperties,用来读取并管理参数
1 2 3 4 5 6 7 8 9 10 11 | @ConfigurationProperties(prefix = "custom")//属性前缀 public class JDBCProperties { public static final String DEFAULT_URL = "localhost"; public String url = DEFAULT_URL; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } } |
在属性参数文件application.properties 中,读取前缀为custom的属性。如针对上述代码,配置中应写:
1 | custom.url = localhost:3306.... |
初始化装载
这是最核心的地方
首先利用@Conditional等注解判断业务类是否存在。若存在则继续。
@EnableConfigurationProperties注解来加载配置参数对象
编写Resolver方法,利用参数初始化装载业务Bean
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Configuration @ConditionalOnClass({ DemoJDBCService.class }) //判断业务类是否存在 @EnableConfigurationProperties(JDBCProperties.class) //加载配置参数对象 public class JDBCAutoConfiguration { @Resource private JDBCProperties jdbcProperties; //开始装载Bean @Bean @ConditionalOnMissingBean(DemoJDBCService.class) @ConditionalOnProperty(name = "custom.url.enabled", matchIfMissing = true) //配置中加个属性,灵活控制开关 public DemoJDBCService jdbcResolver() { DemoJDBCService jdbcService = new DemoJDBCService(); jdbcService.setUrl(jdbcProperties.getUrl()); return jdbcService; } } |
spring.factories
配置spring.factories,添加上刚刚定义的自动配置类。用于运行时扫描自动加载类时使用,否则将无法加载该配置
1 2 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.i3geek.springboot.demo.autoConfig.JDBCAutoConfiguration |
这里是采用系统的自动装载扫描类EnableAutoConfiguration,也可以用户自行编写。(具体后面会详细介绍)
扫描加载配置
自动扫描的过程可以自行编写,本例中采用spring中自带的流程进行源码讲解。
主函数通过@EnableAutoConfiguration注解,利用其内@Import方法导入利用AutoConfigurationImportSelector类
利用AutoConfigurationImportSelector类完成spring.factories文件的扫描,从而加载配置。
@EnableAutoConfiguration
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) //关键 public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {}; } |
@Import , 这个注解可以导入一个配置类到另一个配置类中。在 Spring4.2 中对这个注解进行了加强,可以直接将一个类加入Spring容器。那么只要写上 @Import(AutoConfigurationImportSelector.class) 即可。
AutoConfigurationImportSelector类
该类是实现与ImportSelector接口,该接口作用与注解 @Import类似。
1 2 3 4 5 6 7 8 | public interface ImportSelector {
/** * Select and return the names of which class(es) should be imported based on * the {@link AnnotationMetadata} of the importing @{@link Configuration} class. */ String[] selectImports(AnnotationMetadata importingClassMetadata); } |
其中核心方法selectImports,根据 importingClassMetadata 的值,从带有注解 @Configuration 的类中选择并返回合适的类名数组,将其导入 Spring 容器。因此,查看AutoConfigurationImportSelector类中的ImportSelector方法,就是导入自动配置的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!this.isEnabled(annotationMetadata)) { return NO_IMPORTS; } else { AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader); AnnotationAttributes attributes = this.getAttributes(annotationMetadata); List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);//关键,获得类名列表 configurations = this.removeDuplicates(configurations);//去除重复 Set exclusions = this.getExclusions(annotationMetadata, attributes); this.checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = this.filter(configurations, autoConfigurationMetadata); this.fireAutoConfigurationImportEvents(configurations, exclusions); return StringUtils.toStringArray(configurations); } } |
可见本方法中,主要是getCandidateConfigurations 方法获取类名,之后经过一些处理,把名返回完成spring的导入。
1 2 3 4 5 6 | public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { List configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());//从META-INF/spring.factories中获取 Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."); return configurations; } |
getCandidateConfigurations方法中通过 SpringFactoriesLoader.loadFactoryNames 扫描 spring.factories 文件获得类名。这里就不再深入看了。
Main函数测试
1 2 3 4 5 6 7 8 9 10 11 12 | @RestController @EnableAutoConfiguration //扫描自动配置类,并进行加载 public class JDBCAutoConfigDemo { @Resource private DemoJDBCService jdbcService;
@RequestMapping("/") String test() { return "url:"+ jdbcService.getUrl(); } //main函数省略 } |
如未说明则本站原创,转载请注明出处:爱上极客 » Spring Boot源码分析——自动配置