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[]。在文本配置文件中,通常都是把二进制数据 “编码” 为 Base64Hex 字符串来进行表示。

生成二进制数据

以常见的 AES 密钥为例,生成一个随机的 256 位 AES 密钥,并且以 Base64Hex 格式进行输出:

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));
    }
}

二进制密钥的 Base64Hex 编码值如下:

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 并未对 Base64Hex 形式的二进制数据提供自动转换为字节数组机制。所以,应用启动会抛出异常:

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-keyhex-key 定义了两个 Setter 方法,运行时 Spring Boot 会调用这两个方法注入字符串形式的配置属性,于是我们可以在方法中手动解码 Base64Hex 格式的字符串为字节数组。

测试,在测试方法中注入 AppProp Bean,然后输出 base64KeyhexKey 属性字段对应的字符串编码值:

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)无关,所以兼容性更好。