点击上方 IT牧场 ,选择 置顶或者星标
技术干货每日送达
这是关于配置中心的系列文章,会分多篇发布,内容大致包括:
1、Spring 是如何实现 @Value 注入的
2、一个简易版配置中心的关键技术
3、开源主流配置中心相关技术分享
@Value 注入过程
从一个最简单的程序开始:
@Configuration
@PropertySource("classpath:application.properties")
public class ValueAnnotationDemo {
@Value("${username}")
private String username;
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueAnnotationDemo.class);
System.out.println(context.getBean(ValueAnnotationDemo.class).username);
context.close();
}
}
application.properties 文件内容:
username=coder-xiao-hei
由 AutowiredAnnotationBeanPostProcessor
负责来处理 @Value
,此外该类还负责处理 @Autowired
和 @Inject
。
![](https://i-blog.csdnimg.cn/blog_migrate/1460903338ee3bf24120eddcb6a11946.png)
在 AutowiredAnnotationBeanPostProcessor
中有两个内部类:AutowiredFieldElement
和 AutowiredMethodElement
。
当前为 Field 注入,定位到 AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject
方法。
![](https://i-blog.csdnimg.cn/blog_migrate/ec6332d3278f641069174e9576fb1048.png)
通过 debug 可知,整个调用链如下:
AutowiredFieldElement#inject
-> DefaultListableBeanFactory#resolveDependency
-> DefaultListableBeanFactory#doResolveDependency
-> AbstractBeanFactory#resolveEmbeddedValue
![](https://i-blog.csdnimg.cn/blog_migrate/35513d9f98c20c179d6182901200cef0.png)
通过上述的 debug 跟踪发现可以通过调用 ConfigurableBeanFactory#resolveEmbeddedValue
方法可以获取占位符的值。
![](https://i-blog.csdnimg.cn/blog_migrate/e080d8b927419d1c55a39f70a52fc653.png)
这里的 resolver
是一个 lambda 表达式,继续 debug 我们可以找到具体的执行方法:
![](https://i-blog.csdnimg.cn/blog_migrate/01c5ad6bb7462a9bea90468a798239f7.png)
到此,我们简单总结下:
@Value
的注入由AutowiredAnnotationBeanPostProcessor
来提供支持在
AutowiredAnnotationBeanPostProcessor
中通过调用ConfigurableBeanFactory#resolveEmbeddedValue
来获取占位符具体的值ConfigurableBeanFactory#resolveEmbeddedValue
其实是委托给了ConfigurableEnvironment
来实现
Spring Environment
Environment 概述
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-environment
The Environment interface is an abstraction integrated in the container that models two key aspects of the application environment: profiles and properties.
A profile is a named, logical group of bean definitions to be registered with the container only if the given profile is active. Beans may be assigned to a profile whether defined in XML or with annotations. The role of the Environment object with relation to profiles is in determining which profiles (if any) are currently active, and which profiles (if any) should be active by default.
Properties play an important role in almost all applications and may originate from a variety of sources: properties files, JVM system properties, system environment variables, JNDI, servlet context parameters, ad-hoc Properties objects, Map objects, and so on. The role of the Environment object with relation to properties is to provide the user with a convenient service interface for configuring property sources and resolving properties from them.
Environment 是对 profiles 和 properties 的抽象:
实现了对属性配置的统一存储,同时 properties 允许有多个来源
通过 Environment profiles 来实现条件化装配 Bean
现在我们主要来关注 Environment 对 properties 的支持。
StandardEnvironment
下面,我们就来具体看一下 AbstractApplicationContext#finishBeanFactoryInitialization
中的这个 lambda 表达式。
strVal -> getEnvironment().resolvePlaceholders(strVal)
首先,通过 AbstractApplicationContext#getEnvironment
获取到了 ConfigurableEnvironment
的实例对象,这里创建的其实是 StandardEnvironment
实例对象。
在 StandardEnvironment
中,默认添加了两个自定义的属性源,分别是:systemEnvironment 和 systemProperties。
![](https://i-blog.csdnimg.cn/blog_migrate/31f97ec38d5f9e4158f7ed22aaa1a8f0.png)
也就是说,@Value
默认是可以注入 system properties 和 system environment 的。
PropertySource
StandardEnvironment
继承了 AbstractEnvironment
。
在 AbstractEnvironment
中的属性配置被存放在 MutablePropertySources
中。同时,属性占位符的数据也来自于此。
![](https://i-blog.csdnimg.cn/blog_migrate/ebd47828781bfc84f501cbaf12b6d8de.png)
MutablePropertySources
中存放了多个 PropertySource
,并且这些 PropertySource
是有顺序的。
![](https://i-blog.csdnimg.cn/blog_migrate/1a44c5bfcd165c7d5a6f8a89073dc4e4.png)
PropertySource
是 Spring 对配置属性源的抽象。
![](https://i-blog.csdnimg.cn/blog_migrate/808586ad9fdd7d6db14148e7747f672c.png)
name 表示当前属性源的名称。source 存放了当前的属性。
读者可以自行查看一下最简单的基于 Map
的实现:MapPropertySource
。
配置属性源
有两种方式可以进行属性源配置:使用 @PropertySource
注解,或者通过 MutablePropertySources
的 API。例如:
@Configuration
@PropertySource("classpath:application.properties")
public class ValueAnnotationDemo {
@Value("${username}")
private String username;
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValueAnnotationDemo.class);
Map<String, Object> map = new HashMap<>();
map.put("my.name", "coder小黑");
context.getEnvironment()
.getPropertySources()
.addFirst(new MapPropertySource("coder-xiaohei-test", map));
}
}
总结
Spring 通过
PropertySource
来抽象配置属性源,PropertySource
允许有多个。MutablePropertySources
在 Spring 容器启动的时候,会默认加载 systemEnvironment 和 systemProperties。
StandardEnvironment#customizePropertySources
我们可以通过
@PropertySource
注解或者MutablePropertySources API
来添加自定义配置属性源Environment
是 Spring 对 profiles 和 properties 的抽象,默认实现是StandardEnvironment
@Value
的注入由AutowiredAnnotationBeanPostProcessor
来提供支持,数据源来自于PropertySource
public class Demo {
@Value("${os.name}") // 来自 system properties
private String osName;
@Value("${user.name}") // 通过 MutablePropertySources API 来注册
private String username;
@Value("${os.version}") // 测试先后顺序
private String osVersion;
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(Demo.class);
ConfigurableEnvironment environment = context.getEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
Map<String, Object> source = new HashMap<>();
source.put("user.name", "xiaohei");
source.put("os.version", "version-for-xiaohei");
// 添加自定义 MapPropertySource,且放在第一位
propertySources.addFirst(new MapPropertySource("coder-xiao-hei-test", source));
// 启动容器
context.refresh();
Demo bean = context.getBean(Demo.class);
// Mac OS X
System.out.println(bean.osName);
// xiaohei
System.out.println(bean.username);
// version-for-xiaohei
System.out.println(bean.osVersion);
// Mac OS X
System.out.println(System.getProperty("os.name"));
// 10.15.7
System.out.println(System.getProperty("os.version"));
// xiaohei
System.out.println(environment.getProperty("user.name"));
//xiaohei
System.out.println(environment.resolvePlaceholders("${user.name}"));
context.close();
}
}
简易版配置中心
@Value 支持配置中心数据来源
@Value
的值都来源于 PropertySource
,而我们可以通过 API 的方式来向 Spring Environment 中添加自定义的 PropertySource
。
在此处,我们选择通过监听 ApplicationEnvironmentPreparedEvent
事件来实现。
@Slf4j
public class CentralConfigPropertySourceListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
private final CentralConfig centralConfig = new CentralConfig();
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
centralConfig.loadCentralConfig();
event.getEnvironment().getPropertySources().addFirst(new CentralConfigPropertySource(centralConfig));
}
static class CentralConfig {
private volatile Map<String, Object> config = new HashMap<>();
private void loadCentralConfig() {
// 模拟从配置中心获取数据
config.put("coder.name", "xiaohei");
config.put("coder.language", "java");
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模拟配置更新
config.put("coder.language", "java222");
System.out.println("update 'coder.language' success");
}).start();
}
}
static class CentralConfigPropertySource extends EnumerablePropertySource<CentralConfig> {
private static final String PROPERTY_SOURCE_NAME = "centralConfigPropertySource";
public CentralConfigPropertySource(CentralConfig source) {
super(PROPERTY_SOURCE_NAME, source);
}
@Override
@Nullable
public Object getProperty(String name) {
return this.source.config.get(name);
}
@Override
public boolean containsProperty(String name) {
return this.source.config.containsKey(name);
}
@Override
public String[] getPropertyNames() {
return StringUtils.toStringArray(this.source.config.keySet());
}
}
}
通过 META-INF/spring.factories
文件来注册:
org.springframework.context.ApplicationListener=com.example.config.CentralConfigPropertySourceListener
实时发布更新配置
一般来说有两种方案:
客户端拉模式:客户端长轮询服务端,如果服务端数据发生修改,则立即返回给客户端
服务端推模式:发布更新配置之后,由配置中心主动通知各客户端
在这里我们选用服务端推模式来进行实现。在集群部署环境下,一旦某个配置中心服务感知到了配置项的变化,就会通过 redis 的 pub/sub 来通知客户端和其他的配置中心服务节点
轻量级实现方案,代码简单,但强依赖 redis,pub/sub 可以会有丢失
自定义注解支持动态更新配置
Spring 的 @Value
注入是在 Bean 初始化阶段执行的。在程序运行过程当中,配置项发生了变更, @Value
并不会重新注入。
我们可以通过增强 @Value
或者自定义新的注解来支持动态更新配置。这里小黑选择的是第二种方案,自定义新的注解。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigValue {
String value();
}
@Component
public class ConfigValueAnnotationBeanPostProcessor implements BeanPostProcessor, EnvironmentAware {
private static final PropertyPlaceholderHelper PROPERTY_PLACEHOLDER_HELPER =
new PropertyPlaceholderHelper(
SystemPropertyUtils.PLACEHOLDER_PREFIX,
SystemPropertyUtils.PLACEHOLDER_SUFFIX,
SystemPropertyUtils.VALUE_SEPARATOR,
false);
private MultiValueMap<String, ConfigValueHolder> keyHolder = new LinkedMultiValueMap<>();
private Environment environment;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
ReflectionUtils.doWithFields(bean.getClass(),
field -> {
ConfigValue annotation = AnnotationUtils.findAnnotation(field, ConfigValue.class);
if (annotation == null) {
return;
}
String value = environment.resolvePlaceholders(annotation.value());
ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, bean, value);
String key = PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(annotation.value(), placeholderName -> placeholderName);
ConfigValueHolder configValueHolder = new ConfigValueHolder(bean, beanName, field, key);
keyHolder.add(key, configValueHolder);
});
return bean;
}
/**
* 当配置发生了修改
*
* @param key 配置项
*/
public void update(String key) {
List<ConfigValueHolder> configValueHolders = keyHolder.get(key);
if (CollectionUtils.isEmpty(configValueHolders)) {
return;
}
String property = environment.getProperty(key);
configValueHolders.forEach(holder -> ReflectionUtils.setField(holder.field, holder.bean, property));
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@AllArgsConstructor
static class ConfigValueHolder {
final Object bean;
final String beanName;
final Field field;
final String key;
}
}
主测试代码:
@SpringBootApplication
public class ConfigApplication {
@Value("${coder.name}")
String coderName;
@ConfigValue("${coder.language}")
String language;
public static void main(String[] args) throws InterruptedException {
ConfigurableApplicationContext context = SpringApplication.run(ConfigApplication.class, args);
ConfigApplication bean = context.getBean(ConfigApplication.class);
// xiaohei
System.out.println(bean.coderName);
// java
System.out.println(bean.language);
ConfigValueAnnotationBeanPostProcessor processor = context.getBean(ConfigValueAnnotationBeanPostProcessor.class);
// 模拟配置发生了更新
TimeUnit.SECONDS.sleep(10);
processor.update("coder.language");
// java222
System.out.println(bean.language);
}
}
你点的每个好看,我都认真当成了喜欢
干货分享
最近将个人学习笔记整理成册,使用PDF分享。关注我,回复如下代码,即可获得百度盘地址,无套路领取!
•001:《Java并发与高并发解决方案》学习笔记;•002:《深入JVM内核——原理、诊断与优化》学习笔记;•003:《Java面试宝典》•004:《Docker开源书》•005:《Kubernetes开源书》•006:《DDD速成(领域驱动设计速成)》•007:全部•008:加技术群讨论
近期热文
•LinkedBlockingQueue vs ConcurrentLinkedQueue•解读Java 8 中为并发而生的 ConcurrentHashMap•Redis性能监控指标汇总•最全的DevOps工具集合,再也不怕选型了!•微服务架构下,解决数据库跨库查询的一些思路•聊聊大厂面试官必问的 MySQL 锁机制
关注我
喜欢就点个"在看"呗^_^