对于springboot配置自动刷新,原理也很简单,就是在启动过程中用一个BeanPostProcessor去收集需要自动刷新的字段,然后在springboot启动后开启轮询任务即可。
不过需要对之前的代码再次做修改,因为springboot的配置注入@value("${}"),允许多个${}和嵌套,所以不能确定简单的确定用到了那个配置,本文为了简单就把所有的配置都认为需要动态刷新,实际用的时候可以在application.yml中配置需要动态刷新的配置id列表。代码在https://gitee.com/summer-cat001/config-center。其中设计到的原理都在之前的一篇文章中,感兴趣可以去看看springboot配置注入增强(二)属性注入的原理_springboot bean属性增强-CSDN博客
新增注解
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigRefresh {
}
加上这个注解的字段并且字段上有@value注解就会自动刷新
收集自动刷新的字段
这里会收集自动刷新的字段,并加到ConfigCenterClient的refreshFieldValueList中。长轮询会从这里取数据进行对比,如果发生变化就更新bean中的字段
@Slf4j
public class ConfigRefreshAnnotationBeanPostProcessor implements ApplicationRunner, BeanPostProcessor, BeanFactoryAware, EnvironmentAware {
private Environment environment;
private ConfigurableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (!(beanFactory instanceof ConfigurableBeanFactory)) {
log.warn("ConfigurableBeanFactory requires a ConfigurableListableBeanFactory");
return;
}
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, final String beanName) throws BeansException {
if (beanFactory != null) {
ReflectionUtils.doWithFields(bean.getClass(), field -> {
try {
ConfigRefresh configRefresh = AnnotationUtils.getAnnotation(field, ConfigRefresh.class);
if (configRefresh == null) {
return;
}
Value valueAnnotation = AnnotationUtils.getAnnotation(field, Value.class);
if (valueAnnotation == null) {
return;
}
String value = valueAnnotation.value();
String relValue = beanFactory.resolveEmbeddedValue(value);
ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);
configCenterClient.addRefreshFieldValue(bean, field, relValue);
} catch (Exception e) {
log.error("set bean field fail,beanName:{},fieldName:{}", bean.getClass().getName(), field.getName(), e);
}
});
}
return bean;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void run(ApplicationArguments args) {
ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);
configCenterClient.startSpringBootLongPolling((ConfigurableEnvironment) environment, beanFactory);
}
}
把该bean注入到springboot中,即在spring.factories中加入自动注入
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.config.center.autoconfigure.ConfigAutoConfiguration
这是一个ImportSelector会自动注入返回的类
@Import(ConfigAutoConfiguration.class)
public class ConfigAutoConfiguration implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{ConfigRefreshAnnotationBeanPostProcessor.class.getName()};
}
}
启动长轮询
springboot启动完成后会发一个ApplicationRunner事件,我们只要在实现这个接口的bean中启动即可
@Override
public void run(ApplicationArguments args) {
ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);
configCenterClient.startSpringBootLongPolling((ConfigurableEnvironment) environment, beanFactory);
}
public void startSpringBootLongPolling(ConfigurableEnvironment environment, ConfigurableBeanFactory beanFactory) {
if (configMap.isEmpty() || refreshFieldValueList.isEmpty()) {
log.info("configMap.size:{} refreshFieldValueList.size:{}", configMap.size(), refreshFieldValueList.size());
return;
}
MutablePropertySources propertySources = environment.getPropertySources();
MapPropertySource configCenter = (MapPropertySource) propertySources.get(PROPERTY_SOURCE_NAME);
if (configCenter == null) {
log.warn("configCenter is null");
return;
}
Map<String, Object> source = configCenter.getSource();
Thread thread = new Thread(() -> {
while (!Thread.interrupted()) {
try {
Map<String, Integer> configIdMap = configMap.values().stream().collect(Collectors.toMap(c -> c.getId() + "", ConfigBO::getVersion));
HttpRespBO httpRespBO = HttpUtil.httpPostJson(url + "/config/change/get/long", JSON.toJSONString(configIdMap), 30000);
List<ConfigVO> configList = httpResp2ConfigVOList(httpRespBO);
if (configList.isEmpty()) {
continue;
}
configList.forEach(configVO -> {
Map<String, Object> result = new HashMap<>();
DataTransUtil.buildFlattenedMap(result, configVO.getConfigData(), "");
ConfigBO configBO = this.configMap.get(configVO.getId());
configBO.setVersion(configVO.getVersion());
List<ConfigDataBO> configDataList = configBO.getConfigDataList();
Map<String, ConfigDataBO> configDataMap = configDataList.stream()
.collect(Collectors.toMap(ConfigDataBO::getKey, Function.identity()));
result.forEach((key, value) -> {
ConfigDataBO configDataBO = configDataMap.get(key);
if (configDataBO == null) {
configDataList.add(new ConfigDataBO(key, value.toString()));
} else {
configDataBO.setValue(value.toString());
source.put(key, value);
}
});
});
refreshFieldValueList.forEach(refreshFieldBO -> {
try {
Field field = refreshFieldBO.getField();
Value valueAnnotation = AnnotationUtils.getAnnotation(field, Value.class);
if (valueAnnotation == null) {
return;
}
String value = valueAnnotation.value();
String relValue = beanFactory.resolveEmbeddedValue(value);
if(relValue.equals(refreshFieldBO.getValue())){
return;
}
field.setAccessible(true);
field.set(refreshFieldBO.getBean(), relValue);
} catch (Exception e) {
log.error("startSpringBootLongPolling set Field error", e);
}
});
} catch (Exception e) {
log.error("startSpringBootLongPolling error", e);
}
}
});
thread.setName("startSpringBootLongPolling");
thread.setDaemon(true);
thread.start();
}
效果
@Value
@Data
@Component
public class ConfigTest {
@ConfigRefresh
@Value("${user.name}")
private String name;
}
@Autowired
private ConfigTest configTest;
@Test
public void configTest() throws InterruptedException {
while (true) {
System.out.println(configTest.getName());
Thread.sleep(1000);
}
}
@ConfigurationProperties
增加同时有@ConfigurationProperties和@ConfigRefresh的收集
ConfigRefresh configRefresh = AnnotationUtils.findAnnotation(bean.getClass(), ConfigRefresh.class);
if (configRefresh != null) {
ConfigurationProperties configurationProperties = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);
if (configurationProperties != null) {
ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);
configCenterClient.addRefreshBeanList(bean);
}
}
在长轮询的返回中对@ConfigurationProperties重新绑定
refreshBeanList.forEach(refreshBean -> {
ConfigurationProperties configurationProperties = AnnotationUtils.findAnnotation(refreshBean.getClass(), ConfigurationProperties.class);
if (configurationProperties == null) {
log.warn("refreshBeanList refreshBean configurationProperties is null, class:{}", refreshBean.getClass());
return;
}
Binder binder = Binder.get(environment);
binder.bind(configurationProperties.prefix(), Bindable.ofInstance(refreshBean));
});
完整代码
@Slf4j
public class ConfigRefreshAnnotationBeanPostProcessor implements ApplicationRunner, BeanPostProcessor, BeanFactoryAware, EnvironmentAware {
private Environment environment;
private ConfigurableBeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (!(beanFactory instanceof ConfigurableBeanFactory)) {
log.warn("ConfigurableBeanFactory requires a ConfigurableListableBeanFactory");
return;
}
this.beanFactory = (ConfigurableBeanFactory) beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, final String beanName) throws BeansException {
if (beanFactory != null) {
ReflectionUtils.doWithFields(bean.getClass(), field -> {
try {
ConfigRefresh configRefresh = AnnotationUtils.getAnnotation(field, ConfigRefresh.class);
if (configRefresh == null) {
return;
}
Value valueAnnotation = AnnotationUtils.getAnnotation(field, Value.class);
if (valueAnnotation == null) {
return;
}
String value = valueAnnotation.value();
String relValue = beanFactory.resolveEmbeddedValue(value);
ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);
configCenterClient.addRefreshFieldValue(bean, field, relValue);
} catch (Exception e) {
log.error("set bean field fail,beanName:{},fieldName:{}", bean.getClass().getName(), field.getName(), e);
}
});
ConfigRefresh configRefresh = AnnotationUtils.findAnnotation(bean.getClass(), ConfigRefresh.class);
if (configRefresh != null) {
ConfigurationProperties configurationProperties = AnnotationUtils.findAnnotation(bean.getClass(), ConfigurationProperties.class);
if (configurationProperties != null) {
ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);
configCenterClient.addRefreshBeanList(bean);
}
}
}
return bean;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void run(ApplicationArguments args) {
ConfigCenterClient configCenterClient = ConfigCenterClient.getInstance(null);
configCenterClient.startSpringBootLongPolling((ConfigurableEnvironment) environment, beanFactory);
}
}
public void startSpringBootLongPolling(ConfigurableEnvironment environment, ConfigurableBeanFactory beanFactory) {
if (configMap.isEmpty() || refreshFieldValueList.isEmpty()) {
log.info("configMap.size:{} refreshFieldValueList.size:{}", configMap.size(), refreshFieldValueList.size());
return;
}
MutablePropertySources propertySources = environment.getPropertySources();
MapPropertySource configCenter = (MapPropertySource) propertySources.get(PROPERTY_SOURCE_NAME);
if (configCenter == null) {
log.warn("configCenter is null");
return;
}
Map<String, Object> source = configCenter.getSource();
Thread thread = new Thread(() -> {
while (!Thread.interrupted()) {
try {
Map<String, Integer> configIdMap = configMap.values().stream().collect(Collectors.toMap(c -> c.getId() + "", ConfigBO::getVersion));
HttpRespBO httpRespBO = HttpUtil.httpPostJson(url + "/config/change/get/long", JSON.toJSONString(configIdMap), 30000);
List<ConfigVO> configList = httpResp2ConfigVOList(httpRespBO);
if (configList.isEmpty()) {
continue;
}
configList.forEach(configVO -> {
Map<String, Object> result = new HashMap<>();
DataTransUtil.buildFlattenedMap(result, configVO.getConfigData(), "");
ConfigBO configBO = this.configMap.get(configVO.getId());
configBO.setVersion(configVO.getVersion());
List<ConfigDataBO> configDataList = configBO.getConfigDataList();
Map<String, ConfigDataBO> configDataMap = configDataList.stream()
.collect(Collectors.toMap(ConfigDataBO::getKey, Function.identity()));
result.forEach((key, value) -> {
ConfigDataBO configDataBO = configDataMap.get(key);
if (configDataBO == null) {
configDataList.add(new ConfigDataBO(key, value.toString()));
} else {
configDataBO.setValue(value.toString());
source.put(key, value);
}
});
});
refreshFieldValueList.forEach(refreshFieldBO -> {
try {
Field field = refreshFieldBO.getField();
Value valueAnnotation = AnnotationUtils.getAnnotation(field, Value.class);
if (valueAnnotation == null) {
return;
}
String value = valueAnnotation.value();
String relValue = beanFactory.resolveEmbeddedValue(value);
if (relValue.equals(refreshFieldBO.getValue())) {
return;
}
field.setAccessible(true);
field.set(refreshFieldBO.getBean(), relValue);
} catch (Exception e) {
log.error("startSpringBootLongPolling set Field error", e);
}
});
refreshBeanList.forEach(refreshBean -> {
ConfigurationProperties configurationProperties = AnnotationUtils.findAnnotation(refreshBean.getClass(), ConfigurationProperties.class);
if (configurationProperties == null) {
log.warn("refreshBeanList refreshBean configurationProperties is null, class:{}", refreshBean.getClass());
return;
}
Binder binder = Binder.get(environment);
binder.bind(configurationProperties.prefix(), Bindable.ofInstance(refreshBean));
});
} catch (Exception e) {
log.error("startSpringBootLongPolling error", e);
}
}
});
thread.setName("startSpringBootLongPolling");
thread.setDaemon(true);
thread.start();
}
效果
@Component
@ConfigRefresh
@ConfigurationProperties(prefix = "user")
public class ConfigTest2 {
private String name;
private int age;
private List<String> education;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public List<String> getEducation() {
return education;
}
public void setEducation(List<String> education) {
this.education = education;
}
}
@Autowired
private ConfigTest2 configTest2;
@Test
public void configTest() throws InterruptedException {
while (true) {
System.out.println(configTest2.getName() + "-" + configTest2.getAge() + "-" + configTest2.getEducation());
Thread.sleep(1000);
}
}