一. 背景
前一篇文章系统介绍了@Value
的最佳实践,本篇文章将记录Springboot外部化配置的另一个主角@ConfigurationProperties
。
二. @ConfigurationProperties的最佳实践
在Springboot中很多组件能够实现开箱即用,零配置。比如引入一个spring-boot-redis-starter
,即可使用本地的redis。之所以能够实现零配置能力,是因为依赖了@ConfigurationProperties
的能力。
2.1 基本使用方式
@ConfigurationProperties能够让我们的配置模块化。这样在使用、维护时便于集中管理,各个模块配置之间不会相互影响。我们也可以使用@Value、Environment、启动参数等等方式来实现。但是在模块化功能的场景下显然不适用。
@ConfigurationProperties的基本使用如下:
自定义配置类
/**
* @author sunliming11
* @date created in 2021/2/17
*/
@Data
@ToString
@Component
@ConfigurationProperties(prefix = "my.plugin")
public class MyPluginProperties {
/**
* 是否开启
*/
private Boolean enabled;
/**
* 插件名称
*/
private String name;
/**
* 别名
*/
private List<String> alias;
/**
* map属性
*/
private Map<String, String> map;
/**
* obj
*/
private Security security;
@Data
@ToString
public static class Security {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 角色列表
*/
private List<String> roles = new ArrayList<>(Collections.singleton("USER"));
}
}
引入jar包
需要引入spring-boot-configuration-processor
包,该包最主要的能力是提供了ConfigurationMetadataAnnotationProcessor
这个后置处理器,它的作用就是为了@ConfigurationProperties
生成【元数据】文件。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
配置
在配置之前,我们先build一下工程。如果是IDEA的话,则点击一下右上角的🔨:
之后在编译目录下就能够看到:
此时我们在yml配置文件中进行配置就可以看到相关提示:
YAML完整的配置如下:
my:
plugin:
name: 插件名称
enabled: true
alias:
- 别名1
- 别名2
- 别名3
map:
"[key1]": value1
"[key2]": value2
"/key3": value3
security:
password: 12345
username: 用户名
roles:
- USER
- ADMIN
如果使用的是properties配置方式,则完整配置如下:
my.plugin.name=插件名称
my.plugin.enabled=true
my.plugin.alias[0]=别名1
my.plugin.alias[1]=别名2
my.plugin.alias[2]=别名3
my.plugin.map[key1]=value1
my.plugin.map[key2]=value2
my.plugin.map[key3]=value3
my.plugin.security.username=用户名
my.plugin.security.password=12345
my.plugin.security.roles[0]=ADMIN
my.plugin.security.roles[1]=USER
以上例子展示了如何通过@ConfigurationProperties
来配置一个POJO,可以轻松注入复杂属性类型。但@Value
就则需要借助奇技淫巧才能实现复杂类型的注入。
2.2 relaxed binding
springboot支持@ConfigurationProperties
的宽松绑定规则。例如acme.myProject.person.firstName
这属性的配置和一下是同等效果的。
acme.my-project.person.first-name=LEON
acme.my_project.person.first_name=LEON
ACME_MYPROJECT_PERSON_FIRSTNAME=LEON
...
基于宽松绑定原则,以上书写方式最终都是可以被解析的。也就是说对于同一个单词,只要去掉驼峰式、下划线(\_)、短横线(-)
规则之后的纯字母最终是相同的(不区分大小写),就可以被解析。
2.3 激活的最佳方式
使用@ConfigurationProperties
注解,并不能直接使用,我们还要激活这个配置类。如果没有激活这个配置类的话,Springboot工程依然可以正常启动,但是该配置类不能用。
激活@ConfigurationProperties
的方式有很多种方式,原则上能够将配置类注入到容器中就算激活。基于这个原则,有以下方式激活:
2.3.1 @Component
@Component // 通过@Component并被容器scan到,可以注入到容器中。
@ConfigurationProperties(prefix = "my.plugin")
public class MyPluginProperties {
……
}
2.3.2 @Configuration
@Configuration
@ConfigurationProperties(prefix = "my.plugin")
public class MyPluginProperties {
……
}
2.3.3 @Bean
@Configuration
public class MyConfiguration {
@Bean
public MyPluginProperties myPluginProperties() {
return new MyPluginProperties();
}
}
2.3.4 @EnableConfigurationProperties
@Configuration
@EnableConfigurationProperties(MyPluginProperties.class)
public class MyConfiguration {
}
在源码注释中,@EnableConfigurationProperties
的作用就是提供对@ConfigurationProperties
的支持。如何支持的呢?在源码中类的定义上有:@Import(EnableConfigurationPropertiesRegistrar.class)
,也就是说它说通过@Import的能力来提供支持的。EnableConfigurationPropertiesRegistrar
实现了ImportBeanDefinitionRegistrar
,因此可以注入自定义BeanDefinition
,这里的自定义bd的原始Class就是@EnableConfigurationProperties
注解中指定的value。有了bd自然就可以生成bean。
这么多种使用方式,哪一个比较好呢?其实没有好坏之分。但还是建议使用@EnableConfigurationProperties
,专门的人做专门的事嘛。
2.4 属性or属性值无法匹配怎么办
如果配置了多余的属性比如以上例子中多配置了一个my.plugin.name1
my:
plugin:
name: 插件名称
name1: 多余的配置
默认情况下,工程会正常启动。如果想让工程启动失败,则可以配置ignoreUnknownFields = false
:
@ConfigurationProperties(prefix = "my.plugin", ignoreUnknownFields = false)
如果属性类型不匹配的话,则默认会启动失败,如果不想启动失败,则可以配置ignoreInvalidFields = true
:
@ConfigurationProperties(prefix = "my.plugin", ignoreInvalidFields = true)
2.5 校验
Spring的校验机制基于JSR303,Spring也支持对@ConfigurationProperties
配置类的属性基于JSR303的校验。使用的方式很简单,如下:
@Data
@ToString
@Component
@ConfigurationProperties(prefix = "my.plugin", ignoreUnknownFields = false, ignoreInvalidFields = true)
@Validated //开启校验
public class MyPluginProperties {
/**
* 是否开启
*/
private Boolean enabled;
/**
* 插件名称
*/
@NotNull
@Size(min = 12, max = 16)
private String name;
/**
* 别名
*/
@NotNull
private List<String> alias;
/**
* map属性
*/
private Map<String, String> map;
/**
* obj
*/
private Security security;
在类上开启@Validated
就可以了。然后使用普通的JSR303支持的校验注解即可。
2.6 "零配置"原理
在springboot当中,如何实现0配置的呢?比如引入spring-boot-redis-starter
,不需要做任何配置就可以启动项目,连接本地的redis数据库使用了。
在@ConfigurationProperties
的基础上还依赖了一个注解:@EnableAutoConfiguration
。该注解依然是借助了@Import
的能力,在源码中可以看到:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class) //委托给AutoConfigurationImportSelector
public @interface EnableAutoConfiguration {
……
}
它的完整链路如下:
@EnableAutoConfiguration
➡️
@Import
➡️
@AutoConfigurationImportSelector
➡️
AutoConfigurationImportSelector
➡️
实现selectImports()
➡️
内部调用getAutoConfigurationEntry()
➡️
内部调用getCandidateConfigurations
➡️
org.springframework.core.io.support.SpringFactoriesLoader#loadFactoryNames
➡️
内部调用loadSpringFactories()
在该方法中,我么可以看到:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
}
......
在源码中,能够看到FACTORIES_RESOURCE_LOCATION
常量值为:
也就是说,如果使用了@EnableAutoConfiguration
注解,那么Springboot在启动过程中会去扫描所有包路径下的META-INF/spring.factories
,这个文件当中所有类都会被Spring扫描注入。
自定义
基于以上的知识铺垫,新启一个module来自定义一个配置。目录结构如下:
@Data
@ConfigurationProperties(prefix = "my.plugin")
public class MyPluginProperties {
/**
* 插件名称
*/
private String name = "我的自定义插件";
/**
* 是否开启
*/
private Boolean enabled;
}
//****************************************
@EnableConfigurationProperties(MyPluginProperties.class)
public class MyPluginAutoConfiguration {
}
spring.factories文件如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.leon.myplugin.MyPluginAutoConfiguration
然后用maven package一下,再引入到Springboot工程中:
<dependency>
<groupId>com.leon</groupId>
<artifactId>spring-boot-starter-myplugin</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
reimport一下,就可以在yml文件中愉快的使用了:
四. @ConfigurationProperties的不足之处
@ConfigurationProperties
功能非常强大,但是有一个地方比不上@Value
,那就是不支持SpringEL表达式。在官网上这一点也是明确说明了。但这并不妨碍@ConfigurationProperties
成为Springboot中最重要的底层能力之一。
@Value
的原理是:在bean初始化之前,先进行依赖的注入,主要是委托给AutowiredAnnotationBeanPostProcessor
后置处理器完成的;
@ConfigurationProperties
的原理是:基于@Import
的能力通过EnableConfigurationPropertiesRegistrar
来实现配置类的BeanDefinition的注入,然后再进行属性的注入。
二者的实现原理是完全不一样的。
五. 总结
本篇文章主要是讲述@ConfigurationProperties
的使用方式以及工作原理,在这个过程中还演示了"Springboot"零配置的使用案例以及工作原理。最后比较了@ConfigurationProperties
和@Value
的原理区别。