Spring 中的 Bind

#Bind #Spring #配置管理

Bind 是什么?

Spring Boot 的 Bind 机制主要负责将 外部配置(如 application.ymlapplication.properties、环境变量等)绑定到 Java 对象 中。

外部配置绑定优先级:

  1. 命令行参数(java -jar app.jar --db.port=9000)
  2. 操作系统环境变量(export DB_PORT=9000)
  3. jar 包外部的配置文件(/config/application.yaml)
  4. jar 包内部的配置文件(resources/application.yml)

三种绑定方式

@Value

@Component
public class MyService {
    
    @Value("${app.name:default-app}")  // 支持默认值
    private String appName;
    
    @Value("${app.feature.enabled:false}")
    private boolean featureEnabled;
    
    @Value("#{systemProperties['user.home']}")  // SpEL 表达式
    private String userHome;
}

限制:

  • 不支持 JSR-303 校验
  • 不支持复杂对象绑定
  • 不支持松散绑定
  • 无法在配置文件中提示 IDE 支持

@ConfigurationProperties

@ConfigurationProperties( prefix = "") 外部配置文件:

myapp:
  name: 'myapp'
  server:
    host: 1.1.1.1
    port: 80
  security:
    username: admin
    password: 123

配置方式

@Data
@Component 
@ConfigurationProperties(prefix = "myapp")
public class MyAppProperties {
	// 必须要提供 getter setter,可以使用 Lombook 简化
	private String name;
	
	private ServerConfig server;
	
	private ServerSecurity security;
	
	//嵌套配置 创建类, 必须是 public static
	@Data
	public static class ServerConfig {
		private String host;
		private Long port;
	}
	
	@Data 
	public static class ServerSecurity {
		private String username; 
		private String password;
	}
	
}

使用方式

@Configuration
public class AppConfig {
	@Autowired
	private MyAppProperties properties;
}
  • prefix 的使用: prefix 填入 要被装载的属性的前缀,如果有多个嵌套则可以 myapp.server.XXX.XXX 来装载对应层级下的属性

  • 松散绑定: Spring 可以自动将 配置属性的名词与定义的类中的名称进行自动映射 支持的类型名称:

    • kebab-case (烤肉串式): 全小写,单词间用连字符分隔,如 my-variable-name
    • snake_case (蛇形): 全小写,单词间用下划线分隔,如 my_variable_name
    • UPPERCASE (全大写): 全部大写,通常用于常量,如 MY_CONSTANT
  • JSR-303 校验: 在类上添加 @Validated 开启校验

    • @NotNull
    • @Min()
    • @Max()
    • @NotBlank
    • @Email
    • @Size(min = )
    @Data
    @Component
    @ConfigurationProperties(prefix = "myapp")
    @Validated // <--- 开启第一层校验
    public class MyAppProperties {
        @Valid // <--- 关键!告诉 Spring 钻进去校验内部属性
        private ServerConfig server;
        // ...
    }
    

Binder API

使用 Binder API 读取 Environment 中的配置

Bind bind = Binder.get(environment);

读取单个值

// 读取单个值
String host = bind.bind("myapp.server.host", String.class).orElse("1.1.1.1");

读取单个值要写入完整的路径,并且使用点号分割


读取复杂类型 (List/Map)

如果在 application.yaml 里写了一个 List:

my-app:
  whitelist:
    - 192.168.1.1
    - 10.0.0.1

需要使用 Bindable 包装器处理范型

List<String> ips = 
binder.bind( "my-app.whitelist", Bindable.listOf(String.class) // <--- 关键!告诉 Binder 这是一个 List<String> ).orElse(Collections.emptyList());

如果是更复杂的 比如

routes:  
  - id: "order-service"  
    path: "/api/order"  
    url: "http://localhost:8081"  
  - id: "user-service"  
    path: "/api/user"  
    url: "http://localhost:8082"
    
private List<Map<Object, Object>> routes = new ArrayList<>();
1.方法一
@SuppressWarnings("unchecked")  
List<Map<Object, Object>> routes = (List<Map<Object, Object>>) (List<?>)  
        binder.bind("gateway.routes", Bindable.listOf(Map.class))  
                .orElse(List.of());
2.方法二                  
List<?> routes = binder.bind("gateway.routes", Bindable.listOf(Map.class))  
        .orElse(List.of());

3.方法三
public class RouteConfig {
    private String id;
    private String path;
    private String url;
}

List<RouteConfig> routes = binder.bind("gateway.routes", 
        Bindable.listOf(RouteConfig.class))
        .orElse(List.of());

读取整个配置类

//后面的 XXX.class 是在告诉 “我要读取的是什么类型的配置请转换成这种类型 "
MyAppProperties properties = bind.bind("myapp", MyAppProperties.class).get();

创建本地 bind 并绑定

创建本地 bind 并添加配置

Binder binder = Binder.get(environment);

//添加 配置
1. 方法一
	MapConfigurationPropertySource source1 = new MapConfigurationPropertySource(map1);
	MapConfigurationPropertySource source2 = new MapConfigurationPropertySource(map2);
	
	Binder localBind = new Binder(source1, source2);
2. 方法二
	List<ConfigurationPropertySource> sources = new ArrayList<>();
	sources.add(new MapConfigurationPropertySource(map1));
	sources.add(new MapConfigurationPropertySource(map2));
	sources.add(new MapConfigurationPropertySource(map3));

	Binder localBind = new Binder(sources);
	

生产中建议

private Binder createLocalBinder(Map<String, Object>... configMaps) {
    List<ConfigurationPropertySource> sources = new ArrayList<>();
    
    for (Map<String, Object> map : configMaps) {
        if (map != null && !map.isEmpty()) {
            sources.add(new MapConfigurationPropertySource(map));
        }
    }
    
    return new Binder(sources);
}
// 使用
Binder localBind = createLocalBinder(legacyMap, otherMap, thirdMap);

将 bind 中的值绑定类

//RouteDefinition.java
@Data  
public class RouteDefinition {  
    private String id;  
  
    private String path;  
  
    private String url;  
}

Map<String, Object> legacyMap = new HashMap<>();  
legacyMap.put("id", "legacy-service-999");  
legacyMap.put("path", "/api/old");  
legacyMap.put("url", "http://192.168.1.100");  

//Bind 无法直接绑定 Map 需要转换
MapConfigurationPropertySource legacyConfigurationMap = new MapConfigurationPropertySource(legacyMap);  
Binder localBind = new Binder(legacyConfigurationMap);  

									//为什么这里是 "" 空字符串?
RouteDefinition legacyRoute = localBind.bind("", RouteDefinition.class).orElse(new RouteDefinition());  
System.out.println(legacyRoute);

prefix + 要注入的字段名 = key 此时 key 与 字段名完全相同,所以使用空字符串 如果 legacyMap 中 put 时 key 是 legacy.route.id 那么在将 source 中的属性绑定到 RouteDefinition 时 prefix = “legacy.route”


三种绑定方式对比

特性 @Value @ConfigurationProperties Binder API
使用场景 1-2 个简单配置 结构化配置、多个相关配置 动态绑定、运行时绑定
JSR-303 校验 ❌ 不支持 ✅ 支持(@Validated) ✅ 支持
松散绑定 ❌ 不支持 ✅ 支持 ✅ 支持
复杂对象 ❌ 不支持 ✅ 支持(嵌套对象) ✅ 支持
IDE 元数据支持 ❌ 无提示 ✅ 有提示(spring-configuration-metadata.json) ❌ 无提示
类型安全 ⚠️ 编译时不检查 ✅ 强类型检查 ✅ 强类型检查
集合类型 ⚠️ 需要手动解析 ✅ 自动绑定 List/Set/Map ✅ 自动绑定(需要 Bindable)
默认值 ${prop:default} ✅ 字段初始化或 @DefaultValue .withDefaultValue()
灵活性
  1. 优先使用 @ConfigurationProperties - 类型安全、IDE 支持、可校验
  2. 简单配置用 @Value - 如端口、开关等单个属性
  3. 动态配置用 Binder API - 如从数据库、Nacos 读取配置后绑定

Reference