【基础系列】 编程式属性绑定Binder

文章目录
  1. I. 项目配置
    1. 1. 依赖
    2. 2. 启动入口
  2. II. 实例演示
    1. 1. 基础知识点Binder
    2. 2. 实例演示
      1. 2.1 配置绑定到POJO属性类
      2. 2.2 配置绑定到List对象
      3. 2.3 配置绑定到Map对象
      4. 2.4 配置转换处理
      5. 2.5 绑定方法回调
    3. 3. 小结
  3. III. 不能错过的源码和相关知识点
    1. 0. 项目
    2. 1. 微信公众号: 一灰灰Blog

SpringBoot中极大的简化了项目中对于属性配置的加载方式,可以简单的通过 @Value, @ConfigurationProperties 来实现属性配置与Java POJO对象、Bean的成员变量的绑定,那如果出现一个某些场景,需要我们手动的、通过编程式的方式,将属性配置与给定的pojo对象进行绑定,我们又应该怎么实现呢?

I. 项目配置

1. 依赖

首先搭建一个标准的SpringBoot项目工程,相关版本以及依赖如下

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

2. 启动入口

我们使用默认的配置进行测试,因此启动入口也可以使用最基础的

1
2
3
4
5
6
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}

II. 实例演示

1. 基础知识点Binder

本文的目的主要是给大家介绍编程式的属性绑定,当然除了我们最熟悉的直接写代码,从Environment中获取配置之外,还可以使用Binder来更方便的实现属性绑定

因此我们首先来了解一下这个不太常出现在CURD的日常工作中的Binder类:

Binder JavaDoc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取binder实例
public static Binder get(Environment environment) {
return get(environment, (BindHandler)null);
}


// 属性绑定
// Bind the specified target Class using this binder's property sources.
<T> BindResult<T> bind(String name, Class<T> target)
<T> BindResult<T> bind(String name, Bindable<T> target, BindHandler handler)


// Bind the specified target Class using this binder's property sources or create a new instance using the type of the Bindable if the result of the binding is null.
<T> T bindOrCreate(String name, Class<T> target)
<T> T bindOrCreate(String name, Bindable<T> target, BindHandler handler)

两种常见的使用姿势:

  • bind方法: 将属性绑定到对应的类上, 不会返回null
  • bindOrCreate: 将属性绑定到对应的类上, 返回结果可能为null

2. 实例演示

接下来我们看几个常见的使用姿势

2.1 配置绑定到POJO属性类

直接将配置绑定到我们自定义的属性配置类上,也就是我们最常见的、可直接利用@ConfigurationProperties来实现的使用方式

我们在配置文件中,添加一个基础的配置

1
2
3
4
5
6
7
demo:
mail:
host: smtp.163.com
from: xhhuiblog@163.com
username: test
password: testpwd
port: 465

接下来定义一个对应的属性配置类Mail

1
2
3
4
5
6
7
8
@Data
public class Mail {
private String host;
private String port;
private String user;
private String password;
private String from;
}

然后我们的使用姿势,将如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Component
public class BindHelper implements EnvironmentAware {
private Environment environment;

@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}


public void bindInfo() {
// 直接将前缀对应的配置,加载到指定的对象中
Binder binder = Binder.get(environment);


// 直接绑定到配置类
Mail mail = binder.bindOrCreate("demo.mail", Mail.class);
System.out.println("mail = " + mail);
}

}

在上面的基础使用姿势之上,我们再加两个使用姿势

  • 配置不存在时,返回什么?
  • 使用bind对于不存在时,如何表现

微调一下上面的bindInfo()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void bindInfo() {
// 直接将前缀对应的配置,加载到指定的对象中
Binder binder = Binder.get(environment);


// 直接绑定到配置类
Mail mail = binder.bindOrCreate("demo.mail", Mail.class);
System.out.println("mail = " + mail);

mail = binder.bindOrCreate("demo.mail2", Mail.class);
System.out.println("mail = " + mail);

try {
mail = binder.bind("demo.mail2", Mail.class).get();
System.out.println("mail = " + mail);
} catch (Exception e) {
// 因为配置不存在,会报错
System.out.println(e.getMessage());
}
}

执行之后,输出如下

从上面的输出可以看出,对于

  • bindOrCreat 而言,若整个配置不存在,返回一个空对象,内部属性为null; bind 若相关的配置不存在,会抛异常 (这个不存在指的是配置前缀demo.mail2的都没有)
  • 配置内的某个属性不存在,如 demo.mail.user 这个配置不存在时(配置中的是username),此时bind/bindOrCrate 返回的对象中,相关的属性是null (主意这种场景 bind 方法调用不会抛移异常,有兴趣的小伙伴可以实际验证一下)

2.2 配置绑定到List对象

在实际的应用场景中,配置为数组的可能性也很高,比如我有一个代理库,对应的相关配置如下

1
2
3
4
5
6
demo:
proxy:
- ip: 127.0.0.1
port: 1080
- ip: localhost
port: 1800

此时我们的实际使用姿势可以如下

  • 首先定义Proxy类
1
2
3
4
5
@Data
public class Proxy {
private String ip;
private Integer port;
}

对应的手动绑定方式

1
2
3
4
// 将配置绑定到list
List<Proxy> proxyList = binder.bind("demo.proxy", Bindable.listOf(Proxy.class)).get();
// 或者直接使用 binder.bindOrCreate("demo.proxy", Bindable.listOf(Proxy.class))
System.out.println("list config: " + proxyList);

输出结果如下

1
list config: [BindHelper.Proxy(ip=127.0.0.1, port=1080), BindHelper.Proxy(ip=localhost, port=1800)]

2.3 配置绑定到Map对象

将属性配置绑定到一个Map的场景也不算少见,如之前写过的多数据源自主切换的实现方式中,就有这么个场景

我们写一个简单的配置模拟上面的场景

1
2
3
4
5
6
7
8
demo:
dynamic:
master:
user: main
password: m1
slave:
user: slave
password: s1

在上面的配置中,master/slave 为数据源名称,在下面的配置则为数据源配置信息,结构都一致;基于此,我们需要声明的配置类实际为

1
2
3
4
5
@Data
public class DsConfig {
private String user;
private String password;
}

配置绑定的实现也很简单,与上面List的类似

1
2
Map<String, DsConfig> dsMap = binder.bind("demo.dynamic", Bindable.mapOf(String.class, DsConfig.class)).get();
System.out.println("Map Config: " + dsMap);

执行之后的输出结果如下

1
Map Config: {master=BindHelper.DsConfig(user=main, password=m1), slave=BindHelper.DsConfig(user=slave, password=s1)}

2.4 配置转换处理

上面介绍的姿势都是直接将配置绑定到对应的java对象上,那么我们是否会存在需要对配置属性进行特殊处理的场景呢?

这种场景当然也不算少见,如驼峰与下划线的互转,如密码之类的配置文件中属于加密填写,应用加载时需要解密之后使用等

对于这种场景,我们也给出一个简单的实例,在配置文件中,添加一个base64加密的数据

1
2
3
demo:
enc:
pwd: 5LiA54Gw54GwYmxvZw==

对应的解析方式

1
2
3
4
// 对配置进行解析
String decPwd = binder.bind("demo.enc.pwd", Bindable.of(String.class))
.map(s -> new String(Base64Utils.decodeFromString(s))).get();
System.out.println("解码之后的数据是: " + decPwd);

执行之后,实际输出结果如下:

1
解码之后的数据是: 一灰灰blog

2.5 绑定方法回调

除了上面介绍到的属性绑定姿势之外,Binder还非常贴心的给大家提供了过程回调,给你提供更灵活的控制方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 注册绑定过程回调
String dec = binder.bindOrCreate("demo.enc.pwd", Bindable.of(String.class), new BindHandler() {
@Override
public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
System.out.println("开始绑定: " + name);
return BindHandler.super.onStart(name, target, context);
}

@Override
public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
System.out.println("绑定成功!" + name + " val:" + target.getValue() + " res: " + result);
return new String(Base64Utils.decodeFromString((String) result));
}

@Override
public Object onCreate(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
System.out.println("创建: " + name + " val:" + target.getValue() + " res: " + result);
return BindHandler.super.onCreate(name, target, context, result);
}

@Override
public Object onFailure(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Exception error) throws Exception {
System.out.println("绑定失败! " + name + " " + error.getMessage());
return BindHandler.super.onFailure(name, target, context, error);
}

@Override
public void onFinish(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) throws Exception {
System.out.println("绑定结束: " + name + " val:" + target.getValue() + " res: " + result);
BindHandler.super.onFinish(name, target, context, result);
}
});
System.out.println("绑定回调: " + dec);

同样是实现配置解密,如上面的方式也是可行的,对应的输出如

1
2
3
4
开始绑定: demo.enc.pwd
绑定成功!demo.enc.pwd val:null res: 5LiA54Gw54GwYmxvZw==
绑定结束: demo.enc.pwd val:null res: 一灰灰blog
绑定回调: 一灰灰blog

3. 小结

本文的知识点比较简单,属于看过就会的范畴,但是它的实际应用场景可以说非常多;特别是当我们在某些场景下,直接使用SpringBoot的属性配置绑定不太好实现时,如动态数据源、配置的回调处理等,不妨考虑借助Binder来实现编程式的配置绑定加载

其次本文只介绍了Binder类的使用姿势,有好气的小伙伴,自然会想了解它的具体实现姿势,它是怎么实现配置属性与java实体类进行绑定的呢? 类型转换如何支持的呢? 如果让我们自己来实现配置绑定,可以怎么支持呢?

不妨再进一步,让我们实现一个自定义的配置加载、解析、绑定并注入到Spring容器的解决方案,可以怎么整?

III. 不能错过的源码和相关知识点

0. 项目

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

一灰灰blog


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到