Spring Boot 配置和绑定二进制数据
Spring Boot 中的 application.yaml
/ application.properties
配置文件用于定义应用运行时需要的配置属性。
Spring Boot 提供了强大的配置属性绑定功能,可以把配置文件中的属性绑定到 Java Bean,并且会根据 Java Bean 的字段类型对配置属性进行必要的转换。
绑定属性到 Bean
通过一个简单的示例来看看如何把配置文件中的配置属性绑定到 Bean,并且自动转换其类型。
本文使用的 Spring Boot 版本是 3.3.1。
创建 Spring Boot 项目
创建任意 Spring Boot 应用。
定义配置属性
在 src/main/resources
目录下创建 application.yaml
配置文件,并在其中定义如下自定义的配置属性:
app:
# 数值
port: 8080
# 字符串
title: "Spring Boot 属性绑定测试"
# Duration
duration: 15s
# DataSize
data-size: 10MB
# 集合
file-types: ["png", "jpeg"]
如上,在配置文件中定义了几个不同类型的配置属性。在本例中,这些配置属性的名称没有任何意义,随意取的。
定义配置 Bean
在 cn.springdoc.demo.prop
包下定义与配置文件中属性相对应的 Bean。
package cn.springdoc.demo.prop;
import java.time.Duration;
import java.util.Set;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.unit.DataSize;
import lombok.Data;
@Data // 使用 Lombok 简化 Bean 定义
@ConfigurationProperties(prefix = "app")
public class AppProp {
// int 类型
private int port;
// String 类型
private String title;
// Duration 类型
private Duration duration;
// DataSize 类型
private DataSize dataSize;
// Set 类型
private Set<String> fileTypes;
}
如上,通过 @ConfigurationProperties(prefix = "app")
注解指定配置属性的前缀(用于配置分组)。
配置 Bean 的字段名称和配置文件中的属性名称一一对应,YAML 中使用短横线风格,Java Bean 中使用驼峰。
其中的 DataSize
类型是由 Spring 提供的一种用于描述 “数据大小” 的类型,它对常见的数据单位提供了封装(BYTES、KILOBYTES、MEGABYTES、GIGABYTES、TERABYTES)。
定义 @ConfigurationProperties 类的扫描路径
最后,还需要通过 @ConfigurationPropertiesScan
注解来告诉 Spring 扫描、加载哪些包下的 @ConfigurationProperties
类。
package cn.springdoc.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
@SpringBootApplication
// 扫描指定包下的所有属性配置类
@ConfigurationPropertiesScan("cn.springdoc.demo.prop")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
测试
创建测试类,在测试类中注入 @ConfigurationProperties
类,并且输出其配置内容:
package cn.springdoc.demo.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import cn.springdoc.demo.DemoApplication;
import cn.springdoc.demo.prop.AppProp;
@SpringBootTest(classes = DemoApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {
@Autowired
AppProp appProp;
@Test
public void test () {
System.out.println(appProp);
}
}
如上,在测试类中注入 AppProp
,然后输出其所有字段值。
运行测试,输出如下:
AppProp(port=8080, title=Spring Boot 属性绑定测试, duration=PT15S, dataSize=10485760B, fileTypes=[png, jpeg])
一切 Ok,所有属性都被成功地绑定了,并且 Spring Boot 自动把配置文件中以字符串形式定义的所有配置值都转换为了 Java Bean 中字段所对应的类型。
绑定二进制数据
二进制数据,即 Java 中的字节数组 byte[]
。在文本配置文件中,通常都是把二进制数据 “编码” 为 Base64 或 Hex 字符串来进行表示。
生成二进制数据
以常见的 AES 密钥为例,生成一个随机的 256 位 AES 密钥,并且以 Base64 和 Hex 格式进行输出:
package cn.springdoc.demo.test;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HexFormat;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
public class DemoApplicationMainTests {
public static void main(String[] args) throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
// 256 位,即 32 字节
keyGenerator.init(256, new SecureRandom());
SecretKey secretKey = keyGenerator.generateKey();
byte[] key = secretKey.getEncoded();
System.out.println("Hex:" +HexFormat.of().formatHex(key));
System.out.println("Base64:" + Base64.getEncoder().encodeToString(key));
}
}
二进制密钥的 Base64 和 Hex 编码值如下:
Hex:613416bdedc51d0551a4ff1a8713bb37e13fd1147ada5f997b9e7b3dfc230dea
Base64:YTQWve3FHQVRpP8ahxO7N+E/0RR62l+Ze557PfwjDeo=
在 application.yaml
中定义上面生成的 AES 密钥:
app:
base64-key: "YTQWve3FHQVRpP8ahxO7N+E/0RR62l+Ze557PfwjDeo="
hex-key: "613416bdedc51d0551a4ff1a8713bb37e13fd1147ada5f997b9e7b3dfc230dea"
在 AppProp
中定义对应的属性字段:
package cn.springdoc.demo.prop;
import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;
@Data
@ConfigurationProperties(prefix = "app")
public class AppProp {
private byte[] base64Key;
private byte[] hexKey;
}
但,问题是 Spring Boot 并未对 Base64 和 Hex 形式的二进制数据提供自动转换为字节数组机制。所以,应用启动会抛出异常:
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2024-08-01T12:59:59.869+08:00 ERROR 6036 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'app.base64-key' to byte[]:
Property: app.base64-key
Value: "YTQWve3FHQVRpP8ahxO7N+E/0RR62l+Ze557PfwjDeo="
Origin: class path resource [application.yaml] - 4:15
Reason: failed to convert java.lang.String to byte (caused by java.lang.NumberFormatException: For input string: "YTQWve3FHQVRpP8ahxO7N+E/0RR62l+Ze557PfwjDeo=")
Action:
Update your application's configuration
异常信息很明白,无法把字符串解析为数值(failed to convert java.lang.String to byte)。
手动解码
一种简单、直接的解决办法就是,手动解码配置属性。
package cn.springdoc.demo.prop;
import java.util.Base64;
import java.util.HexFormat;
import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;
@Data
@ConfigurationProperties(prefix = "app")
public class AppProp {
private byte[] base64Key;
private byte[] hexKey;
// 通过 Setter 注入 base64Key
public void setBase64Key(String key) {
this.base64Key = Base64.getDecoder().decode(key);
}
// 通过 Setter 注入 hexKey
public void setHexKey(String key) {
this.hexKey = HexFormat.of().parseHex(key);
}
}
如上,在配置类中专门为配置属性 base64-key
和 hex-key
定义了两个 Setter 方法,运行时 Spring Boot 会调用这两个方法注入字符串形式的配置属性,于是我们可以在方法中手动解码 Base64 或 Hex 格式的字符串为字节数组。
测试,在测试方法中注入 AppProp
Bean,然后输出 base64Key
和 hexKey
属性字段对应的字符串编码值:
package cn.springdoc.demo.test;
import java.util.Base64;
import java.util.HexFormat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import cn.springdoc.demo.DemoApplication;
import cn.springdoc.demo.prop.AppProp;
@SpringBootTest(classes = DemoApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {
@Autowired
AppProp appProp;
@Test
public void test () {
System.out.println("Base64:" + Base64.getEncoder().encodeToString(appProp.getBase64Key()));
System.out.println("Hex:" + HexFormat.of().formatHex(appProp.getHexKey()));
}
}
运行测试,输出如下:
Base64:YTQWve3FHQVRpP8ahxO7N+E/0RR62l+Ze557PfwjDeo=
Hex:613416bdedc51d0551a4ff1a8713bb37e13fd1147ada5f997b9e7b3dfc230dea
可以看到输出的编码后的值和配置文件中定义的一样,说明配置文件中的二进制值注入成功了。
自动解码
手动解码,多少也有点不够优雅。
还有一种方式,也是推荐的方式,即使用 YAML
规范中的 !!binary
标记来定义 Base64 格式的二进制配置值,这样的话框架就会自动把 Base64 值解码为字节数组。
配置如下:
app:
base64-key: !!binary |
YTQWve3FHQVRpP8ahxO7N+E/0RR62l+Ze557PfwjDeo=
在 YAML 中,使用 !!binary |
指示符告诉解析器这是 Base64 编码的二进制数据,并使用多行字符串格式表示数据。在配置类中,直接定义其对应的 byte[]
字段就行。
package cn.springdoc.demo.prop;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "app")
public record AppProp(byte[] base64Key) {
}
如上,这次使用了 Java 中的 record
来定义属性配置类,它更加简单、直观且属性值都是 final
的,运行时候不会被误修改。这是推荐按的做法。
测试,注入 AppProp
record
类,然后输出 base64Key
字段的值:
@Autowired
AppProp appProp;
@Test
public void test () {
System.out.println("Base64:" + Base64.getEncoder().encodeToString(appProp.base64Key()));
}
输出如下,和配置文件中的配置值一致,说明一切 Ok。
Base64:YTQWve3FHQVRpP8ahxO7N+E/0RR62l+Ze557PfwjDeo=
总结
在文本配置中,往往都是把二进制数据编码为 Base64 格式的字符串进行保存。在 YAML
配置文件中,我们可以通过 !!binary |
指示符来让解析器把 Base64 配置值解析为字节数组,由于这是 YAML
规范定义的,和上层框架(例如 Spring)无关,所以兼容性更好。