SpringBoot开发实用篇之配置高级

本文最后更新于:13 天前

使用@ConfigurationProperties可以为使用@Bean声明的第三方bean绑定属性:添加到类上是为spring容器管理的当前类的对象绑定属性,添加到方法上是为spring容器管理的当前方法的返回值对象绑定属性。

使用@EnableConfigurationProperties可以声明进行属性绑定的bean,即将使用@ConfigurationProperties注解的类加入Spring容器。使用后无需再使用@Component注解再次进行bean声明。

@Value注解不支持松散绑定规则。绑定前缀名推荐采用烤肉串命名规则,仅能使用纯小写字母、数字、下划线作为合法的字符,且必须以字母开头。

开启Bean属性校验功能一共3步:

  1. 导入JSR303与Hibernate校验框架坐标;
  2. 使用@Validated注解启用校验功能;
  3. 使用具体校验规则规范数据校验格式。

yaml文件中对于数字的定义支持进制书写格式,如需使用字符串请使用引号明确标注。

@ConfigurationProperties

在基础篇学习了**@ConfigurationProperties注解,此注解的作用是用来为bean绑定属性的。**

在yml配置文件中以对象的格式添加若干属性:

1
2
3
4
servers:
ip-address: 192.168.0.1
port: 2345
timeout: -1

然后再开发一个用来封装数据的实体类,注意要提供属性对应的setter方法:

1
2
3
4
5
6
7
@Component
@Data
public class ServerConfig {
private String ipAddress;
private int port;
private long timeout;
}

使用@ConfigurationProperties注解就可以将配置中的属性值关联到开发的模型类上

1
2
3
4
5
6
7
8
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
private String ipAddress;
private int port;
private long timeout;
}

这样加载对应bean的时候就可以直接加载配置属性值了。但是目前我们学的都是给自定义的bean使用这种形式加载属性值。而第三方开发的bean源代码不是你自己书写的,也不可能到源代码中去添加 @ConfigurationProperties 注解,能不能用这种形式加载属性值呢?


使用@ConfigurationProperties注解可以为使用@Bean声明的第三方bean绑定属性,只是格式特殊一点。

  1. 使用@Bean注解定义第三方bean

    1
    2
    3
    4
    5
    @Bean
    public DruidDataSource datasource(){
    DruidDataSource ds = new DruidDataSource();
    return ds;
    }
  2. 在yml中定义要绑定的属性,注意datasource此时全小写

    1
    2
    datasource:
    driverClassName: com.mysql.jdbc.Driver
  3. 使用@ConfigurationProperties注解为第三方bean进行属性绑定,注意前缀是全小写的datasource

    1
    2
    3
    4
    5
    6
    @Bean
    @ConfigurationProperties(prefix = "datasource")
    public DruidDataSource datasource(){
    DruidDataSource ds = new DruidDataSource();
    return ds;
    }

操作方式完全一样,只不过@ConfigurationProperties注解不仅能添加到类上,还可以添加到方法上,添加到类上是为spring容器管理的当前类的对象绑定属性,添加到方法上是为spring容器管理的当前方法的返回值对象绑定属性。


目前我们定义bean不是通过类注解定义就是通过@Bean定义,使用@ConfigurationProperties注解可以为bean进行属性绑定,那在一个业务系统中,哪些bean通过注解@ConfigurationProperties去绑定属性了呢?因为这个注解不仅可以写在类上,还可以写在方法上,所以找起来就比较麻烦了。为了解决这个问题,spring给我们提供了一个全新的注解,专门标注使用@ConfigurationProperties注解绑定属性的bean是哪些。这个注解叫做 @EnableConfigurationProperties 。即**@EnableConfigurationProperties用来声明进行属性绑定的bean**。

  1. 在配置类上开启 @EnableConfigurationProperties 注解,并标注要使用 @ConfigurationProperties 注解绑定属性的类

    1
    2
    3
    4
    @SpringBootApplication
    @EnableConfigurationProperties(ServerConfig.class)
    public class Springboot13ConfigurationApplication {
    }
  2. 在对应的类上直接使用 @ConfigurationProperties 进行属性绑定

    1
    2
    3
    4
    5
    6
    7
    @Data
    @ConfigurationProperties(prefix = "servers")
    public class ServerConfig {
    private String ipAddress;
    private int port;
    private long timeout;
    }

    注意观察,现在绑定属性的ServerConfig类时并没有声明@Component注解。当使用@EnableConfigurationProperties注解时,spring会默认将其标注的类定义为bean,因此无需再次声明@Component注解了。(@EnableConfigurationProperties与@Component不能同时使用)


一个小技巧,使用@ConfigurationProperties注解时,会出现一个提示信息:

只需要添加一个坐标此提醒就消失了:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

总结

  • 使用@ConfigurationProperties可以为使用@Bean声明的第三方bean绑定属性
  • 当使用@EnableConfigurationProperties声明进行属性绑定的bean后,无需使用@Component注解再次进行bean声明

宽松绑定/松散绑定

在进行属性绑定时,可能会遇到如下情况,为了进行标准命名,开发者会将属性名严格按照驼峰命名法书写,在yml配置文件中将datasource修改为dataSource,如下:

1
2
dataSource:
driverClassName: com.mysql.jdbc.Driver

此时程序可以正常运行,然后又将代码中的前缀datasource修改为dataSource,如下:

1
2
3
4
5
6
@Bean
@ConfigurationProperties(prefix = "dataSource")
public DruidDataSource datasource(){
DruidDataSource ds = new DruidDataSource();
return ds;
}

此时就发生了编译错误,而且并不是idea工具导致的,运行后依然会出现问题,配置属性名dataSource是无效的:

为什么会出现这种问题,这就要来说一说springboot进行属性绑定时的一个重要知识点了,有关属性名称的宽松绑定,也可以称为宽松绑定。

什么是宽松绑定?实际上是springboot进行编程时人性化设计的一种体现,即配置文件中的命名格式与变量名的命名格式可以进行格式上的最大化兼容,几乎主流的命名格式都支持。例如:

在ServerConfig中的ipAddress属性名:

1
2
3
4
5
6
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {
private String ipAddress;
}

可以与下面的配置属性名规则全兼容

1
2
3
4
5
servers:
ipAddress: 192.168.0.2 # 驼峰模式
ip_address: 192.168.0.2 # 下划线模式
ip-address: 192.168.0.2 # 烤肉串模式
IP_ADDRESS: 192.168.0.2 # 常量模式

也可以说,以上4种模式最终都可以匹配到ipAddress这个属性名。为什么这样呢?原因就是在进行匹配时,配置中的名称要去掉中划线和下划线后,忽略大小写的情况下去与java代码中的属性名进行忽略大小写的等值匹配,以上4种命名去掉下划线中划线忽略大小写后都是一个词ipaddress,java代码中的属性名忽略大小写后也是ipaddress,这样就可以进行等值匹配了,这就是为什么这4种格式都能匹配成功的原因。不过springboot官方推荐使用烤肉串模式,也就是中划线模式。


到这里我们掌握了一个知识点,就是命名的规范问题。再来看开始出现的编程错误信息

其中Reason描述了报错的原因,绑定前缀名命名应该是烤肉串(kebab)模式(case),仅能使用纯小写字母、数字、下划线作为合法的字符,且必须以字母开头。然后再看我们写的名称 dataSource,就不满足上述要求。

因此松散绑定规则仅针对springboot中@ConfigurationProperties注解进行属性绑定时有效,对@Value注解进行属性映射无效。


总结

  • @ConfigurationProperties绑定属性时支持属性名宽松绑定,这个宽松体现在属性名的命名规则上。
  • @Value注解不支持松散绑定规则。
  • 绑定前缀名推荐采用烤肉串命名规则,即使用中划线做分隔符。

常用计量单位绑定

在前面的配置中,我们书写了如下配置值,其中第三项超时时间timeout描述了服务器操作超时时间,当前值是 -1 表示永不超时。

1
2
3
4
servers:
ip-address: 192.168.0.1
port: 2345
timeout: -1

但每个人都这个值的理解会产生不同,比如线上服务器完成一次主从备份,配置超时时间240,这个240如果单位是秒就是超时时间4分钟,如果单位是分钟就是超时时间4小时。面对一次线上服务器的主从备份,设置4分钟,别说拷贝过程,连备份之前的压缩过程4分钟也搞不定,这个时候问题就来了,怎么解决这个误会?

除了加强约定之外,springboot充分利用了JDK8中提供的全新的用来表示计量单位的新数据类型,从根本上解决这个问题。以下模型类中添加了两个JDK8中新增的类,分别是 Duration 和 DataSize :

  • Duration:表示时间间隔,可以通过@DurationUnit注解描述时间单位,例如上例中描述的单位为小时(ChronoUnit.HOURS)。

  • DataSize:表示存储空间,可以通过@DataSizeUnit注解描述存储空间单位,例如上例中描述的单位为MB(DataUnit.MEGABYTES)。

使用上述两个单位就可以有效避免因沟通不同步或文档不健全导致的信息不对称问题,从根本上解决了问题,避免产生误读。


1
2
3
4
5
6
7
8
9
10
11
@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {

@DurationUnit(ChronoUnit.HOURS)
private Duration serverTimeOut;

@DataSizeUnit(DataUnit.BYTES)
private DataSize dataSize;
}

数据校验

在进行属性绑定时虽然有松散绑定规则,但是在书写时由于无法感知模型类中的数据类型,就会出现类型不匹配的问题。比如代码中需要int类型,配置中给了非法的数值,例如写一个 “a”,这种数据肯定无法有效的绑定,还会引发错误。

SpringBoot给出了强大的数据校验功能,可以有效的避免此类问题的发生。在JAVAEE的JSR303规范中给出了具体的数据校验标准,开发者可以根据自己的需要选择对应的校验框架,此处使用Hibernate提供的校验框架来作为实现进行数据校验。

开启Bean属性校验功能一共3步:

  1. 导入JSR303与Hibernate校验框架坐标;
  2. 使用@Validated注解启用校验功能;
  3. 使用具体校验规则规范数据校验格式。

步骤①:开启校验框架

1
2
3
4
5
6
7
8
9
10
<!--1.导入JSR303规范-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<!--使用hibernate框架提供的校验器做实现-->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>

步骤②:在需要开启校验功能的类上使用注解@Validated开启校验功能

1
2
3
4
5
6
7
@Component
@Data
@ConfigurationProperties(prefix = "servers")
//开启对当前bean的属性注入校验
@Validated
public class ServerConfig {
}

步骤③:对具体的字段设置校验规则

1
2
3
4
5
6
7
8
9
10
11
@Component
@Data
@ConfigurationProperties(prefix = "servers")
//开启对当前bean的属性注入校验
@Validated
public class ServerConfig {
//设置具体的规则
@Max(value = 8888,message = "最大值不能超过8888")
@Min(value = 202,message = "最小值不能低于202")
private int port;
}

数据类型转换

线上开发时连接数据库正常操作,密码并没有输入错误,但是运行程序后显示如下信息:

1
java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)

yml中数据库连接信息如下

1
2
3
4
5
6
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=UTC
username: root
password: 0127

这里密码是0127,其实问题就出在这里:

打印出的密码为87,这是因为0127在开发者眼中是一个字符串 “0127”,但是在springboot看来,这就是一个数字,而且是一个八进制的数字。当后台使用String类型接收数据时,如果配置文件中配置了一个整数值,他是先安装整数进行处理,读取后再转换成字符串。0127撞上了八进制的格式,所以最终以十进制数字87的结果存在了。

因此以后开发中,字符串标准书写应加上引号包裹,遇到0开头的数据多注意

yaml中不同数据类型书写格式