【基础系列】自定义属性配置绑定极简实现姿势介绍

文章目录
  1. I. 项目配置
    1. 1. 依赖
    2. 2. 启动入口
  2. II. 自定义配置实现
    1. 1. 方案设计
    2. 2. 实现方式
      1. 2.1 加载自定义配置源
      2. 2.2 扫描需要绑定bean
      3. 2.3 bean与配置的绑定
      4. 2.4 完整实现
    3. 3. 测试验证
  3. III. 不能错过的源码和相关知识点
    1. 0. 项目
    2. 1. 微信公众号: 一灰灰Blog

使用过SpringBoot应用的小伙伴应该对它配套的配置文件application.yml不会陌生,通常我们将应用需要的配置信息,放在配置文件中,然后再应用中,就可以通过 @Value 或者 @ConfigurationProperties来引用

那么配置信息只能放在这些配置文件么? 能否从db/redis中获取配置信息呢? 又或者借助http/rpc从其他的应用中获取配置信息呢?

答案当然是可以,比如我们熟悉的配置中心(apollo, nacos, SpringCloudConfig)

接下来我们将介绍一个不借助配置中心,也可以实现自定义配置信息加载的方式,并且支持配置的动态刷新

I. 项目配置

1. 依赖

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

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

添加web支持,用于配置刷新演示

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

2. 启动入口

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

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

II. 自定义配置实现

1. 方案设计

我们的目标是实现一个自定义的配置信息加载,并支持配置与Spring bean对象的绑定,同时我们还需要支持这个配置的动态刷新

基于上面这个目标,要想实现则需要几个知识储备:

结合上面的知识点,我们主要需要实现的有三步:

  1. 读取自定义的配置
  2. 扫描需要绑定自定义配置的bean
  3. 借助Binder来重新绑定bean中的定义的属性到envionment的配置参数(这里就包含了自定义的配置及默认的配置)

2. 实现方式

为了简化自定义的配置使用,我们这里直接使用一个内存缓存来模拟自定义的配置源

2.1 加载自定义配置源

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
@Component
public class SelfConfigContainer implements EnvironmentAware, ApplicationContextAware {
private ConfigurableEnvironment environment;
private ApplicationContext applicationContext;

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

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

public Map<String, Object> configCache = new HashMap<>();

@PostConstruct
public void init() {
configCache.put("config.type", 12);
configCache.put("config.wechat", "一灰灰blog");

MutablePropertySources propertySources = new MutablePropertySources(environment.getPropertySources());
// 将内存的配置信息设置为最高优先级
MapPropertySource propertySource = new MapPropertySource(namespace, cache);
propertySources.addFirst(propertySource);
}
}

注意上面的实现,这里是自定义的配置源 propertySources 中包含了environment的配置信息;如果希望将自定义的配置信息源注入到environment,可以如下实现

1
2
MapPropertySource propertySource = new MapPropertySource("selfSource", SelfConfigContext.getInstance().getCache());
environment.getPropertySources().addFirst(propertySource);

2.2 扫描需要绑定bean

接下来我们自定义一个注解@ConfDot, 凡是带有这个注解的bean的成员变量,从上面的属性源中进行初始化

这个注解可以完全按照@ConfigurationProperties的来设计(实际上我们也可以直接使用@ConfigurationProperties注解,这样适用范围更广了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfDot {
@AliasFor("prefix")
String value() default "";

@AliasFor("value")
String prefix() default "";

boolean ignoreInvalidFields() default false;

boolean ignoreUnknownFields() default true;
}

然后借助Spring来扫描带有特定注解的bean,就可以很简单了

1
2
3
4
5
applicationContext.getBeansWithAnnotation(ConfDot.class).values().forEach(bean -> {
Bindable<?> target = Bindable.ofInstance(bean)
.withAnnotations(AnnotationUtils.findAnnotation(bean.getClass(), ConfDot.class));
selfConfigBinder.bind(target);
});

2.3 bean与配置的绑定

上面两部完成之后,接下来就需要我们将配置与bean进行绑定,这里就主要使用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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class SelfConfigBinder {
private final ApplicationContext applicationContext;
private PropertySources propertySource;

private volatile Binder binder;

public SelfConfigBinder(ApplicationContext applicationContext, PropertySources propertySource) {
this.applicationContext = applicationContext;
this.propertySource = propertySource;
}

public <T> void bind(String prefix, Bindable<T> bindable) {
getBinder().bind(prefix, bindable, new IgnoreTopLevelConverterNotFoundBindHandler());
}

public <T> void bind(Bindable<T> bindable) {
ConfDot propertiesAno = bindable.getAnnotation(ConfDot.class);
if (propertiesAno != null) {
BindHandler bindHandler = getBindHandler(propertiesAno);
getBinder().bind(propertiesAno.prefix(), bindable, bindHandler);
}
}

private BindHandler getBindHandler(ConfDot annotation) {
BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler();
if (annotation.ignoreInvalidFields()) {
handler = new IgnoreErrorsBindHandler(handler);
}
if (!annotation.ignoreUnknownFields()) {
UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
handler = new NoUnboundElementsBindHandler(handler, filter);
}
return handler;
}

private Binder getBinder() {
if (this.binder == null) {
synchronized (this) {
if (this.binder == null) {
this.binder = new Binder(getConfigurationPropertySources(),
getPropertySourcesPlaceholdersResolver(), getConversionService(),
getPropertyEditorInitializer());
}
}
}
return this.binder;
}

private Iterable<ConfigurationPropertySource> getConfigurationPropertySources() {
return ConfigurationPropertySources.from(this.propertySource);
}

/**
* 指定占位符的前缀、后缀、默认值分隔符、未解析忽略、环境变量容器
*
* @return
*/
private PropertySourcesPlaceholdersResolver getPropertySourcesPlaceholdersResolver() {
return new PropertySourcesPlaceholdersResolver(this.propertySource);
}

/**
* 类型转换
*
* @return
*/
private ConversionService getConversionService() {
return new DefaultConversionService();
}

private Consumer<PropertyEditorRegistry> getPropertyEditorInitializer() {
if (this.applicationContext instanceof ConfigurableApplicationContext) {
return ((ConfigurableApplicationContext) this.applicationContext)
.getBeanFactory()::copyRegisteredEditorsTo;
}
return null;
}
}

上面的实现虽然多,但是核心其实比较简单:

  1. 初始化Binder对象 this.binder = new Binder(getConfigurationPropertySources(), getPropertySourcesPlaceholdersResolver(), getConversionService(), getPropertyEditorInitializer());
  2. 提供绑定入口
1
2
3
4
5
6
7
8
public <T> void bind(Bindable<T> bindable) {
ConfDot propertiesAno = bindable.getAnnotation(ConfDot.class);
if (propertiesAno != null) {
// bindHandler即绑定的处理策略,如没有映射到时,怎么处理
BindHandler bindHandler = getBindHandler(propertiesAno);
getBinder().bind(propertiesAno.prefix(), bindable, bindHandler);
}
}

2.4 完整实现

上面的三步实现,基本上已经将整个功能给实现了,其中SelfConfigBinder提供了完成的代码实现,接下来我们再将第一步与第三步的整合,来看一下完整的实现,并且提供一个配置刷新的支持

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Component
public class SelfConfigContainer implements EnvironmentAware, ApplicationContextAware {
private ConfigurableEnvironment environment;
private ApplicationContext applicationContext;

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

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}

private SelfConfigBinder binder;
public Map<String, Object> configCache = new HashMap<>();

@PostConstruct
public void init() {
configCache.put("config.type", 12);
configCache.put("config.wechat", "一灰灰blog");
bindBeansFromLocalCache("config", configCache);
}

private void bindBeansFromLocalCache(String namespace, Map<String, Object> cache) {
MutablePropertySources propertySources = new MutablePropertySources(environment.getPropertySources());
// 将内存的配置信息设置为最高优先级
MapPropertySource propertySource = new MapPropertySource(namespace, cache);
propertySources.addFirst(propertySource);
this.binder = new SelfConfigBinder(this.applicationContext, propertySources);
refreshConfig(null, null);
}

/**
* 配置绑定
*
* @param bindable
*/
public void bind(Bindable bindable) {
binder.bind(bindable);
}

/**
* 支持配置的动态刷新
*
* @param key
* @param val
*/
public void refreshConfig(String key, String val) {
if (key != null) {
configCache.put(key, val);
}
applicationContext.getBeansWithAnnotation(ConfDot.class).values().forEach(bean -> {
Bindable<?> target = Bindable.ofInstance(bean)
// Bindable.of(ResolvableType.forClass(bean.getClass())).withExistingValue(bean)
.withAnnotations(AnnotationUtils.findAnnotation(bean.getClass(), ConfDot.class));
bind(target);
});
}
}

3. 测试验证

接下来就是验证一下上面的设计,首先再配置文件中,添加几个默认的信息

1
2
3
config:
user: test
pwd: password

绑定配置的bean对象

1
2
3
4
5
6
7
8
9
10
11
@Data
@Component
@ConfDot(prefix = "config")
public class MyConfig {

private String user;

private String pwd;

private Integer type;
}

上面这个MyConfig中的 user, pwd 从前面的配置文件中获取,然后type则此自定义的配置信息configCache中获取,应该是12,接下来我们首先一个访问与刷新的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@RestController
public class IndexController {
@Autowired
private MyConfig myConfig;

@GetMapping(path = "/")
public String hello() {
return JSON.toJSONString(myConfig);
}

@GetMapping(path = "update")
public String updateCache(String key, String val) {
selfConfigContainer.refreshConfig(key, val);
return hello();
}
}

实际执行测试如下图

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

0. 项目

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

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

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

一灰灰blog


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