获取配置文件的属性值

Spring Boot项目里满屏的@Value太扎眼

在Spring Boot项目中我们注入配置属性,可能最简单最直接的方式就是使用 @Value("${placeholder}") 了.

但是如果说配置属性有很多或者配置属性本身分层等情况下,使用@Value就会很麻烦,而且由于其参数是字符串表达式,出错概率相对更高,而且不易排查.

Spring Boot 项目中可以使用配置对象来接收并使用配置属性。它主要有以下几个优势:

  • 类型安全,属性注入可实现必要校验.
  • 默认值优雅处理.
  • 使用方便,作为配置对象来注入使用,调用方法来获取属性值.
  • 支持Meta-Data,主流编辑器都可以识别Meta信息,配置时可以得到友好提示.

1.@ConfigurationProperties 注解

在所有的开端,我们先来了解这个注解:

/**
  * 这里能看到这个注解可以用在类定义和方法定义上,具体的我们后文详解.
  */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface ConfigurationProperties {

    /**
     * 可有效绑定到此对象的属性的前缀.
     * 是{@link #prefix()}的同义属性 {@link #prefix()}. 
     * 一个有效的属性前缀可以是一个或多个用.(点)符号分隔的字符串。
     *  (e.g. {@code "acme.system.feature"}).
     * @return 要绑定的属性前缀
     */
    @AliasFor("prefix")
    String value() default "";

    /**
     * (同上)
     */
    @AliasFor("value")
    String prefix() default "";

    /**
     * 一个表示在向该对象绑定配置属性时,忽略无效的字段.
     * 这里的无效一般指属性类型错误(或者不能强制转换为定义类型).
     * @return 标志值 (默认 false),即默认不忽略.
     */
    Boolean ignoreInvalidFields() default false;

    /**
     * 一个表示向该对象绑定配置属性时,忽略未知的字段.
     * 这里的未知指的是该对象中没有定义的属性,而配置列表中出现的属性是否被忽略.
     * @return 标志值 (默认 true),即默认忽略.
     */
    boolean ignoreUnknownFields() default true;
}

2.定义一个配置对象

定义一个配置对象其实非常简单,就是一个普通的对象定义,然后添加 @ConfigurationProperties 注解.

@ConfigurationProperties(prefix = "jbp")
public class JavaBeanProperties {
    private String Name;
    private Integer version;
    private Boolean enabled;

    public String getName() {
        return name;
    }

    public Integer getVersion() {
        return version;
    }

    public Boolean getEnabled() {
        return enabled;
    }
}

目前上面这个类的定义可还不行,Spring Boot对配置对象有两种方式可以实现配置属性的注入:

使用Setter方法

@ConfigurationProperties(prefix = "jbp")
public class JavaBeanProperties {
   // ......
    public void setName(String name) {
        this.name = name;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }
}

这个就简单易懂了, Spring Boot 在进行属性解析的时候会使用配置对象的 构造器 创建对象,然后根据配置情况,调用对应的 Setter 方式注入属性. 这种方式也是比较常用的方式,定义也很简单.

注意事项

如果类中添加了有参构造器,会出现以下几种情况: 只有一个有参构造器,那么对象初始化的时候会以Spring Bean构造器初始化的方式来调用此构造器,即:此构造器的参数会寻找环境中的Bean来注入,这并不是一般情况下需要的,所以在使用Setter方式时请注意不要添加有参构造器. 在添加了有参构造器的同时添加了无参构造器,此时表现和没有定义任何构造器的情况是一样的,最终会调用无参构造器来初始化配置对象. 添加了多个有参构造器,而没有定义无参构造器,此时配置对象会初始化失败,因为找不到默认构造器.

使用有参构造器

这里就要引入另一个注解了: @ConstructorBinding

/**
  * 这个注解可以被用在类定义上,
  * 也可以被用于类的指定构造器上.
  */
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConstructorBinding {

}

简而言之,就是使用构造器直接在初始化配置对象的时候就注入属性,此时Setter方法就没有必要了. 我们对配置对象改造如下:

@ConfigurationProperties(prefix = "jbp")
@ConstructorBinding
public class JavaBeanProperties {
    private String name;
    private Integer version;
    private Boolean enabled;

    public String getName() {
        return name;
    }

    public Integer getVersion() {
        return version;
    }

    public Boolean getEnabled() {
        return enabled;
    }

    public JavaBeanProperties(String name, Integer version, Boolean enabled) {
        this.name = name;
        this.version = version;
        this.enabled = enabled;
    }
}

我们刚前面看了 @ConstructorBinding 注解的定义,它也支持在构造器方法上使用. 当我们的类定义中有多个构造器的时候,就需要使用此注解明确地指定所要使用的构造器. 就像下面这样:

@ConfigurationProperties(prefix = "jbp")
public class JavaBeanProperties {
    private String name;
    private Integer version;
    private Boolean enabled;

    public String getName() {
        return name;
    }

    public Integer getVersion() {
        return version;
    }

    public Boolean getEnabled() {
        return enabled;
    }

    @ConstructorBinding
    public JavaBeanProperties(String name, Integer version, Boolean enabled) {
        this.name = name;
        this.version = version;
        this.enabled = enabled;
    }

    public JavaBeanProperties(String name, Integer version) {
        this.name = name;
        this.version = version;

}

属性默认值处理

使用 Setter 方式时,在属性定义上直接初始化赋默认值即可:

@ConfigurationProperties(prefix = "jbp")
public class JavaBeanProperties {
    private String name;
    private Integer version=1;
    private Boolean enabled;
}

使用构造器时, 使用 @DefaultValue 注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Documented
public @interface DefaultValue {

    /**
     * 默认值设置. 当属性类型是集合或者数组的时候,默认值可以是一组值.
     * @return 属性默认值.
     */
    String[] value() default {};
}
 @ConstructorBinding
    public JavaBeanProperties(String name,@DefaultValue("1") Integer version, Boolean enabled) {
        this.name = name;
        this.version = version;
        this.enabled = enabled;
    }

这种方式一般在配置对象里面还是很少用的,除非有必要!!!像上述两种方式的替代方式就是在 Getter 方式中做文章或者在构造器做文章,最后在没有配置属性的情况提供一个默认值即可.

3.启用配置对象

我们上面都是在定义配置对象,但是到目前为止这个配置对象其实还只是个普通对象,还不能称之为配置对象. 除非你进行了下面的操作

3.1. @EnableConfigurationProperties 注解

同样,我们先来了解一下这个重要的注解:

/**
  * 这个注解可以被用在配置对象类定义上,
  * 也就是@Configuration注解修饰(或间接修饰)的配置类上.
  */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesRegistrar.class)
public @interface EnableConfigurationProperties {

    /**
     * 这是个声明Spring 配置对象中的校验器,后文我们专门来讲一下这部分.
     * @since 2.2.0
     */
    String VALIDATOR_BEAN_NAME = "configurationPropertiesValidator";

    /**
     * 将使用
     * {@link ConfigurationProperties @ConfigurationProperties} 注解修饰的类注册到Spring容器中.
     * @return {@code @ConfigurationProperties} Bean注册.
     */
    Class<?>[] value() default {};
}

使用 @EnableConfigurationProperties注解是常用,也是比较好的方式,建议大家在定义配置对象的时候尽量都使用此注解来启用.

一般像我们在添加自己的自动配置模块的时候都会有一些自定义配置项添加,这个时候标准的做法就是使用 @ConfigurationProperties 修饰定义的配置对象,之后在 自动配置类 上使用 @EnableConfigurationProperties 来启用各配置对象定义.

添加配置类来启用 JavaBeanProperties 配置对象定义

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JavaBeanProperties.class )
public class JavaBeanConfig {
    //... 
}

3.2. @ConfigurationPropertiesScan注解

其实除了上面的 @EnableConfigurationProperties 注解外,Spring Boot 还提供一个比较少用的方式,这个注解是使用类似于 ComponentScan 一样的方式来扫描配置对象定义.

/**
  * 通常情况下,这个注解都是在@SpringBootApplication注解修饰的
  * 应用程序主启动类上,
  * 当然它其实是支持添加到任何@Configuration类上的.
  * 如果正文的package相关属性没有指定的话,
  * 默认是从此注解标记的类所有包开始扫描.
  * (下面的属性注释就不翻译了,都是Spring Boot中常见的属性.)
  */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ConfigurationPropertiesScanRegistrar.class)
@EnableConfigurationProperties
public @interface ConfigurationPropertiesScan {

    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};
}

但是,这个注解并不推荐被使用,很多时候我们的配置类并不总是适合被直接按包扫描的,这可能带来很多其它问题.尤其是当我们的配置对象本身是根据 @Conditional 条件化启用的时候,用扫描就很容易破坏条件处理.

4. 第三方类作为配置对象

有一些特殊的应用场景,我们可能会想把一个第三方类结构作为一个配置对象来读取我们自己定义的一些属性,作为后续使用. 这里主要对应 @ConfigurationProperties 注解使用在方法上的场景,当将此注解使用在 @Bean 注解生效的方法上的时候,可以达到以下效果.

4.1 示例: 自定义初始化 DataSource

我们都知道一个数据库连接对象的初始化有特别多的参数,这时候如果我们完全自己去定义一个配置对象然后向Spring Boot DataSource转换就实为不智. 我们就可以采用像下面的@Bean方法这样的方式来达到目的.

@Bean
@ConfigurationProperties(prefix = "my.datasource.first")
HikariDataSource dataSource() {
    HikariDataSource dataSource =  new HikariDataSource();
    return dataSource;
}

4.2 示例: 同一个配置对象,处理多个前缀,各自使用

你我们上面自己定义的配置对象,如果我们最终要在三个地方使用,只是它们的参数结构都一样,也没必要一定要再单独定义配置对象.我们可以像下面这样共用.

@Bean
@ConfigurationProperties(prefix = "jbp.common")
JavaBeanProperties commonJavaBeanProperties() {
    JavaBeanProperties jbp =  new JavaBeanProperties();
    return jbp;
}
@Bean
@ConfigurationProperties(prefix = "jbp.first")
JavaBeanProperties firstJavaBeanProperties() {
    JavaBeanProperties jbp =  new JavaBeanProperties();
    return jbp;
}
@Bean
@ConfigurationProperties(prefix = "jbp.second")
JavaBeanProperties secondJavaBeanProperties() {
    JavaBeanProperties jbp =  new JavaBeanProperties();
    return jbp;
}

上面的示例场景中,每个@Bean方法定义会读取指定前缀的配置属性,调用配置对象的Setter方法作最终注入,所以这种情况下,要注意配置对象有没有这些Setter方法.

5. 使用配置对象

5.1 说明

首先,这里说明两点:

当 @ConfigurationProperties 注解在类上使用时,最终注册到Spring容器中的Bean名称会长这样: <prefix>-<配置对象全类名> ,当前缀没有配置时,则为 <配置对象全类名> 当 @ConfigurationProperties 注解在 @Bean 方法上时,其命名和正常的 @Bean 方法生成的Bean命名规则一致.

5.2 一般情况使用示例

定义要使用配置对象的类

public class JavaBeanUsing {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private final JavaBeanProperties javaBeanProperties;

    public JavaBeanUsing(JavaBeanProperties javaBeanProperties) {
        this.javaBeanProperties = javaBeanProperties;
        logger.info("\n\t使用JavaBean配置对象:{}",javaBeanProperties.toString());
    }
}

设置配置参数 application.yml

jbp:
  name: Java Bean 属性对象
  enabled: true
  version: 1

注入配置对象

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JavaBeanProperties.class )
public class JavaBeanConfig {

    @Bean
    public JavaBeanUsing javaBeanUsing(JavaBeanProperties javaBeanProperties){
        return new JavaBeanUsing(javaBeanProperties);
    }
}

启动项目看一下输出是不是和我们的预期一致.

 [main] INFO  c.x.x.m.typesafeconfig.JavaBeanUsing - 

使用JavaBean配置对象:JavaBeanProperties[name='Java Bean 属性对象', version=1, enabled=true]

5.3 使用名称注入

当我们的配置被初始化了多个的时候,我们就不能使用默认的类型自动注入了,这个时候就需要用到名称了,名称规则我们在 上方的 5.1 已经说明.

修改JavaBeanUsing定义

public class JavaBeanUsing {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    public JavaBeanUsing(JavaBeanProperties first,
                       JavaBeanProperties second,
                       JavaBeanProperties base) {
        // 这里分别打印定义的3个配置对象.
    logger.info("使用JavaBean配置对象:\n\tFirst:{},\n\tSecond:{},\n\tBase:{}",first,second,base);
    }
}

模拟多Bean初始化

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(JavaBeanProperties.class )
public class JavaBeanConfig {
    @Bean
    @ConfigurationProperties(prefix = "jbp.first")
    public JavaBeanProperties propertiesFirst(){
        return new JavaBeanProperties();
    }
    @Bean
    @ConfigurationProperties(prefix = "jbp.second")
    public JavaBeanProperties propertiesSecond(){
        return new JavaBeanProperties();
    }

    @Bean
    public JavaBeanUsing javaBeanUsing(
                                    JavaBeanProperties propertiesFirst,
                     JavaBeanProperties propertiesSecond,
    @Autowired @Qualifier("jbp-com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties") JavaBeanProperties javaBeanProperties){
        // 这里3个Bean使用名称对应注入
    return new JavaBeanUsing(propertiesFirst,propertiesSecond,javaBeanProperties);
    }
}

配置属性

jbp:
  name: Java Bean 属性对象
  enabled: true
  version: 1
  first:
    name: First Bean 属性对象
    enabled: false
    version: 2
  second:
    name: Second Bean 属性对象
    enabled: false
    version: 3

重启-验证

[main] INFO  c.c.b.m.typesafeconfig.JavaBeanUsing - 使用JavaBean配置对象:
    First:JavaBeanProperties[name='First Bean 属性对象', version=2, enabled=false],
    Second:JavaBeanProperties[name='Second Bean 属性对象', version=3, enabled=false],
    Base:JavaBeanProperties[name='Java Bean 属性对象', version=1, enabled=true]

6. 配置对象中实现属性校验

我们在 3.1 有看到 @EnableConfigurationProperties 注解中有一个静态变量: VALIDATOR_BEAN_NAME ,它其实是Spring Boot用于初始化默认的属性校验器的名称. 具体怎么初始化的我们这里不展开,我们就来看一下如果使用:

6.1. 引入依赖

Spring Boot中的属性校验都是基于以下依赖开始的:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

6.2. @Validated 与 @Valid

如果之前已经使用过Spring Boot 中的 Validation 相关功能的,应该就对这两个注解有认识;

  • @Validated 是标记当前类开启校验, 在本文中我们就可以用在我们的配置对类定义上.
  • @Valid 是用于嵌套类属性,比如我们在我们的配置对象中添加一个 inner 属性;

定义Inner类

static class Inner{
    private String innerName;
    private Integer innerVersion;
    // Getter Setter ......
}

配置对象修改如下:

@ConfigurationProperties(prefix = "jbp")
@Validated
public class JavaBeanProperties {
    private String name;

    @NotNull
    @Range(min = 1,max = 5)
    private Integer version=1;
    private Boolean enabled;

    @Valid
// 这里添加一个Inner类型的属性.
// 要开启Inner中的属性校验,使用@Valid注解.
    private Inner inner;
    // Getter Setter ......
}

我们的配置参数修改如下:

jbp:
  name: Java Bean 属性对象
  enabled: true
  version: 6
  first:
    name: First Bean 属性对象
    enabled: false
    version: 2
  second:
    name: Second Bean 属性对象
    enabled: false
    version: 3

现在启动项目我们将看到类似下方这样的错误信息:

Binding to target org.springframework.boot.context.properties.bind.BindException: 
Failed to bind properties under 'jbp' to com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties failed:

    Property: jbp.version
    Value: "6"
    Origin: class path resource [application.yaml] - 51:12
    Reason: 需要在1和5之间

Action:

Update your application's configuration

7. Meta-Data支持

我们在Spring Boot项目开发过程中,在像 application.yaml或者application.properties 之类的配置文件中配置属性时,会有这样的体验:Spring Boot Meta-Data Support

这个其实就是由Spring Boot的配置属性 Meta-Data 提供了信息,编辑器读取这些信息后就可以给我们这样友好的提示,可以方便地了解到支持哪些属性,各属性都是什么涵义.

那像上面我们自己定义的配置对象要怎么达到这样的效果呢?

我们来进行经典三步走:

7.1. 依赖添加

以下为Maven中添加依赖.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>
</dependencies>

以下为Gradle中添加依赖

dependencies {
    annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
}

Spring Boot提供了一个元信息生成注解处理器,它会根据我们使用 @ConfigurationProperties 注解修饰类(或@Bean注解的方法)的定义信息在编译过程中向 classes/META-INF/ 目录生成 spring-configuration-metadata.json 文件:

上面我们定义的 JavaBeanProperties 生成的文件内容如下

{
  "groups": [
    {
      "name": "jbp",
      "type": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties"
    },
    {
      "name": "jbp.first",
      "type": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanConfig",
      "sourceMethod": "propertiesFirst()"
    },
    {
      "name": "jbp.second",
      "type": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanConfig",
      "sourceMethod": "propertiesSecond()"
    }
  ],
  "properties": [
    {
      "name": "jbp.enabled",
      "type": "java.lang.Boolean",
      "description": "开关标志.",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties"
    },
    {
      "name": "jbp.first.enabled",
      "type": "java.lang.Boolean",
      "description": "开关标志.",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties"
    },
    {
      "name": "jbp.first.name",
      "type": "java.lang.String",
      "description": "名称.",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties"
    },
    {
      "name": "jbp.first.version",
      "type": "java.lang.Integer",
      "description": "版本号.",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties",
      "defaultValue": 1
    },
    {
      "name": "jbp.name",
      "type": "java.lang.String",
      "description": "名称.",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties"
    },
    {
      "name": "jbp.second.enabled",
      "type": "java.lang.Boolean",
      "description": "开关标志.",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties"
    },
    {
      "name": "jbp.second.name",
      "type": "java.lang.String",
      "description": "名称.",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties"
    },
    {
      "name": "jbp.second.version",
      "type": "java.lang.Integer",
      "description": "版本号.",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties",
      "defaultValue": 1
    },
    {
      "name": "jbp.version",
      "type": "java.lang.Integer",
      "description": "版本号.",
      "sourceType": "com.xxx.xxx.mvc.typesafeconfig.JavaBeanProperties",
      "defaultValue": 1
    }
  ],
  "hints": []
}

7.2. 元信息概述

Spring Boot的 Metadata 信息大概的总体结构如下:

{"groups": [
    {
        "name": "配置属性前缀分组",
        "type": "对应的配置对象类名称",
        "description": "描述信息",
        "sourceType": "一般情况下和type属性值一样."
    }
    ...
],"properties": [
    {
        "name": "属性名",
        "type": "属性类型",
        "sourceType": "属性所在的类",
        "description": "描述信息",
        "defaultValue": "默认值"
    }
    ...
],"hints": [
    ...
]}

Spring Boot生成配置参数的元信息时,会将注释信息做为相关字段的描述信息,同时建议这些注释以英文句号结尾,以保证良好的格式. Group部分信息主要来源于类定义相关信息,名称则为prefix属性值.未定义的话则无. 默认值信息的生成同样基于配置对象默认值的配置,两种方式效果一样. 像我们上面使用 @Bean方法 + @ConfigurationProperties 同样会生成对应的元信息. Meta-Data还有其它自定义的内容,我们后面再讲,平常我们使用的话,上面的内容已经完全足够.

7.3 看下效果

Spring Boot Meta-Data Support

8. 结语

看到这里,相信朋友们已经可以胸有成竹地去将你项目里满屏的@Value清理清理了,换一个态度去面对你的项目,让你的项目也焕发新生!