Springboot应用系列—外部化配置之@ConfigurationProperties最佳实践

3,223 阅读6分钟

一. 背景

  前一篇文章系统介绍了@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的原理区别。