基于SpringBoot手撸配置中心
一、配置中心
什么是配置中心?
在微服务架构中,当系统从一个单体应用,被拆分成分布式系统上一个个服务节点后,配置文件也必须跟着迁移 (分割),配置中心由此产生。配置中心将配置从各应用中剥离出来,对配置进行统一管理,应用自身不需要自己去管理配置。
配置中心的主要作用?
- 统一管理存储配置文件。
- 配置发生变动后,主动通知服务进行配置变更。
二、手写配置中心
背景:基于本地 JSON 格式的文件进行配置读取,当配置发生变更后,手动执行服务提供的 Rest 接口(模拟Zookeeper的事件监听)刷新项目配置。
主要实现:
- 获取配置文件内容,封装为 PropertySource 对象。
- 通过 SpringBoot 的 Environment 添加到 SpringBoot 启动环境变量中。
- 监听配置变更,刷新实体类中的属性值。
2.1 创建一个SpringBoot项目
主要依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
</dependencies>
项目结构及主要实现类
2.2 配置文件读取
定义 PropertySource 解析封装器:用来解析配置文件,封装为 PropertySource 对象。
/**
* 定义 PropertySource 解析封装器:用来解析配置文件,封装为 PropertySource 对象
*/
public interface PropertySourceResolver {
PropertySource<?> load(Environment environment, ConfigurableApplicationContext context);
}
实现 PropertySourceResolver ,用于解析本地 JSON 文件内容。
/**
* 加载本地的 JSON 数据数据源
*/
@Component("localJSONPropertySource")
public class LocalJSONPropertySource implements PropertySourceResolver {
public static final String SOURCE_NAME = "my-properties-json";
private static final String SOURCE_PATH = "F:\\document\\小李架构路\\env\\src\\main\\resources\\myproperties.json";
@Override
public PropertySource<?> load(Environment environment, ConfigurableApplicationContext context) {
PropertySource propertySource = new MapPropertySource(SOURCE_NAME, loadJsonFile());
return propertySource;
}
/**
* 加载json文件
* @return
*/
private Map<String, Object> loadJsonFile() {
File resource = new File(SOURCE_PATH);
if (!resource.exists()) {
throw new RuntimeException(String.format("file[%s] not exist!!!", SOURCE_PATH));
}
try (
InputStream inputStream = new FileInputStream(resource);
) {
StringBuffer sb = new StringBuffer();
byte[] bytes = new byte[1024];
while (inputStream.read(bytes) > -1) {
sb.append(new String(bytes, StandardCharsets.UTF_8));
}
System.out.println("----------加载配置文件内容:\n" + sb.toString());
return JSON.parseObject(sb.toString(), new TypeReference<Map<String, Object>>() {
});
} catch (Exception e) {
e.printStackTrace();
}
return new HashMap<>();
}
}
2.3 通过Environment装载配置
通过 Spring 扩展接口 ApplicationContextInitializer
,在应用上下文刷新前,将 加载封装的 PropertySource 对象添加到 Environment 中。
/**
* SpringBoot refreshContext(context); 之前执行
*/
class EnvApplicationContextInit implements ApplicationContextInitializer {
private List<PropertySourceResolver> propertySourceResolvers;
public EnvApplicationContextInit() {
System.out.println("------------加载PropertySourceResolver------start");
// 通过 SPI 加载所有的 PropertySourceResolver 实现:扩展
propertySourceResolvers = SpringFactoriesLoader.loadFactories(PropertySourceResolver.class, this.getClass().getClassLoader());
System.out.println("------------加载PropertySourceResolver:" + propertySourceResolvers.size());
System.out.println("------------加载PropertySourceResolver------end");
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
// 在当前类,将需要加载的配置源通过 PropertySourceResolver 封装为 PropertySource 添加到 environment 中
if (CollectionUtils.isEmpty(propertySourceResolvers)) {
return;
}
ConfigurableEnvironment environment = applicationContext.getEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
for (PropertySourceResolver propertySourceResolver : propertySourceResolvers) {
PropertySource<?> source = propertySourceResolver.load(environment, applicationContext);
if (source == null){
continue;
}
// 添加 PropertySource 到 environment 中
propertySources.addLast(source);
}
}
}
2.4 缓存需要刷新配置文件的类
定义标注注解,用于标注那些类当配置文件发送变更后,需要刷新属性值。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface RefreshScope {
}
通过 Spring 扩展接口 BeanPostProcessor 接口,缓存需要刷新配置文件的类。
/**
* 收集被 @RefreshScope 注解标注的所有类,方便配置源更新后,对 @Value 进行刷新
*/
@Component
public class RefreshScopeBeanPostProcessor implements BeanPostProcessor {
public static final Map<Object, List<RefreshFieldVO>> BEAN_MAP = new ConcurrentHashMap<>();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
RefreshScope refreshScope = bean.getClass().getAnnotation(RefreshScope.class);
if (refreshScope != null) {
Field[] fields = bean.getClass().getDeclaredFields();
for (Field field : fields) {
Value valueAnno = field.getAnnotation(Value.class);
if (valueAnno != null) {
BEAN_MAP.putIfAbsent(bean, new ArrayList<>());
BEAN_MAP.computeIfPresent(bean, (key, val) -> {
val.add(this.createRefreshFieldVO(field, valueAnno.value()));
return val;
});
}
}
}
return bean;
}
private RefreshFieldVO createRefreshFieldVO(Field field, String valuekey) {
RefreshFieldVO refreshFieldVO = new RefreshFieldVO();
refreshFieldVO.setField(field);
// 解析 valuekey ${xxx:xxxx}
valuekey = getPropertyKey(valuekey);
refreshFieldVO.setValueKey(valuekey);
return refreshFieldVO;
}
private String getPropertyKey(String valuekey) {
int startIndex = valuekey.indexOf("${");
if (startIndex > -1) {
valuekey = valuekey.substring(startIndex + 2);
}
int endIndex = valuekey.indexOf(":");
if (endIndex > -1) {
valuekey = valuekey.substring(0, endIndex);
return valuekey;
}
endIndex = valuekey.indexOf("}");
return valuekey.substring(0, endIndex);
}
}
到这里,就完成了在服务启动是,获取配置,加载到本地环境中。
2.5 配置发生变更,通过事件刷新服务配置
2.5.1 定义配置变更Event
public class RefreshEnvEvent extends ApplicationEvent {
public RefreshEnvEvent(Object source) {
super(source);
}
}
2.5.2 定义配置变更Listener
定义配置变更Listener,用于当收到 配置刷新事件后,主动刷新环境变量。
@Component
public class RefreshEnvListener implements ApplicationListener<RefreshEnvEvent> {
@Autowired
private ApplicationContext applicationContext;
@Resource(name = "localJSONPropertySource")
private LocalJSONPropertySource localJSONPropertySource;
@Override
public void onApplicationEvent(RefreshEnvEvent event) {
System.out.println("---------配置刷新事件--------");
// 刷新替换 Environment
ConfigurableEnvironment environment = (ConfigurableEnvironment) applicationContext.getEnvironment();
environment.getPropertySources().replace(LocalJSONPropertySource.SOURCE_NAME, localJSONPropertySource.load(environment, (ConfigurableApplicationContext) applicationContext));
try {
Map<Object, List<RefreshFieldVO>> beanMap = RefreshScopeBeanPostProcessor.BEAN_MAP;
for (Map.Entry<Object, List<RefreshFieldVO>> entry : beanMap.entrySet()) {
// 刷新属性值
for (RefreshFieldVO refreshFieldVO : entry.getValue()) {
refreshFieldVO.getField().setAccessible(true);
refreshFieldVO.getField().set(entry.getKey(), environment.getProperty(refreshFieldVO.getValueKey()));
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
2.5.3 监听到配置中心变更后,主动发送RefreshEnvEvent
@RestController
@RequestMapping("/env")
public class EnvController {
@Autowired
private ApplicationContext applicationContext;
/**
* 更新事件
* @return
*/
@GetMapping("updateEnv")
public String updateEnv() {
// 发布更新事件,模拟事件监听,如nacos 更新事件、zookeeper 节点变动事件等等....
applicationContext.publishEvent(new RefreshEnvEvent(new Object()));
return "success";
}
}
2.6 配置文件及spring.factories
myproperties.json
{
"test": "我是test-22222222222"
}
spring.factories
# ApplicationContextInitializer
org.springframework.context.ApplicationContextInitializer=\
com.example.env.EnvApplicationContextInit
# PropertySourceResolver:自定义接口,用于扩展配置文件的解析,后续可以自行添加
# 如:添加以zookeeper为配置中,只需实现 PropertySourceResolver 接口,完成配置文件的解析,并且监听zookeeper节点变动事件,服务环境变量刷新事件即可
com.example.env.prop.PropertySourceResolver=\
com.example.env.prop.LocalJSONPropertySource
2.7 测试
@RefreshScope
@RestController
@RequestMapping("/")
public class TestController {
@Value("${test}")
private String test;
@GetMapping("getTest")
public String getTest() {
return test;
}
}