背景
会员管理后台对于首次学生认证的用户发放学生礼包权益需要发放积分礼包以及其他优惠券礼包,如果用户已经有过奖励积分记录,则无需发放积分礼包。礼包被配置成两个,一个包含积分,另外一个则不包含积分。积分的唯一标识配置在apollo之中,由于配置时将两个权益的标识配置反了导致程序没有按预期的效果执行。
并且由于更新管控比较严格,更新窗口为每周二、周四。其他时间更新属于紧急更新,需要产品与客户经理反馈更新原因,并且协调更新人员进行更新。整个链路比较长,紧急是因为一个简单配置导致应用需要重启更新,所以希望可以做到apollo能够实现热更新。
实现思路
从官网上了解到使用@ApolloConfigChangeListener可以监听到apollo的配置修改。定义apollo监听类,并且监听方法加上@ApolloConfigChangeListener即可,可以获取到修改的值、key以及修改类型。拿到这三个信息我们就可以实现热更新了。
我们可以使用观察者模式来完成系统某些配置的热更新。定义ApolloValueChangeEvent接口,通过提供不同的ApolloValueChangeEvent实现类来完成各种因配置变化而产生的一些变更。例如更新@Value注解字段的值等。
具体实现
1、定义AnoValueRefreshPostProcessor后置处理器将spring对象中的@Value注解字段与springbean的映射关系建立起来,方便后续对@Value注解字段进行热更新。
import com.xmair.ecip.core.log.ApiLogger;
import com.xmair.framework.apollo.entity.FieldPair;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author: ljw
* @Date: 2023-11-1 16:37
*/
@Component
public class AnoValueRefreshPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {
private Map<String, List<FieldPair>> mapper = new HashMap<>();
@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
processMetaValue(bean);
return super.postProcessAfterInstantiation(bean, beanName);
}
/**
* 这里主要的目的就是获取支持动态刷新的配置属性,然后缓存起来
*
* @param bean
*/
private void processMetaValue(Object bean) {
try {
Class clz = bean.getClass();
for (Field field : clz.getDeclaredFields()) {
if (field.isAnnotationPresent(Value.class)) {
Value val = field.getAnnotation(Value.class);
List<String> keyList = pickPropertyKey(val.value(), 0);
for (String key : keyList) {
mapper.computeIfAbsent(key, (k) -> new ArrayList<>())
.add(new FieldPair(bean, field, val.value()));
}
}
}
} catch (Exception e) {
ApiLogger.bizLog.warn("AnoValueRefreshPostProcessor processMetaValue warn : "+e);
}
}
/**
* 实现一个基础的配置文件参数动态刷新支持
*
* @param value
* @return 提取key列表
*/
private List<String> pickPropertyKey(String value, int begin) {
int start = value.indexOf("${", begin) + 2;
if (start < 2) {
return new ArrayList<>();
}
int middle = value.indexOf(":", start);
int end = value.indexOf("}", start);
String key;
if (middle > 0 && middle < end) {
// 包含默认值
key = value.substring(start, middle);
} else {
// 不包含默认值
key = value.substring(start, end);
}
List<String> keys = pickPropertyKey(value, end);
keys.add(key);
return keys;
}
public List<FieldPair> fieldPairs(String key){
return mapper.get(key);
}
}
2、定义ApolloValueChangeEvent接口
public interface ApolloValueChangeEvent{
void nodify(PropertyChangeType optType, String key, String value);
}
3、定义FieldPair简化字段赋值
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.core.env.Environment;
import org.springframework.util.PropertyPlaceholderHelper;
import java.lang.reflect.Field;
/**
* @Author: ljw
* @Date: 2023-11-1 16:57
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FieldPair {
private static PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}",
":", true);
Object bean;
Field field;
String value;
public void updateValue(Environment environment) {
boolean access = field.isAccessible();
if (!access) {
field.setAccessible(true);
}
String updateVal = propertyPlaceholderHelper.replacePlaceholders(value, environment::getProperty);
try {
if (field.getType() == String.class) {
field.set(bean, updateVal);
} else {
field.set(bean, JSONObject.parseObject(updateVal, field.getType()));
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
field.setAccessible(access);
}
}
4、定义ValueRefreshEvent实现ApolloValueChangeEvent接口,用于热更新@Value字段。
package com.xmair.framework.apollo.event;
import com.ctrip.framework.apollo.enums.PropertyChangeType;
import com.ulisesbocchio.jasyptspringboot.wrapper.EncryptableEnumerablePropertySourceWrapper;
import com.xmair.ecip.core.log.ApiLogger;
import com.xmair.ecip.core.util.SpringBeanTools;
import com.xmair.framework.apollo.entity.FieldPair;
import com.xmair.framework.spring.processor.AnoValueRefreshPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* @Author: ljw
* @Date: 2023-11-6 10:11
*/
@Component
public class ValueRefreshEvent implements ApolloValueChangeEvent{
private ConfigurableEnvironment environment;
public ValueRefreshEvent(ConfigurableEnvironment environment) {
this.environment = environment;
}
private final String name = "ApolloBootstrapPropertySources";
@Override
public void nodify(PropertyChangeType optType, String key, String value) {
try {
EncryptableEnumerablePropertySourceWrapper compositePropertySource = (EncryptableEnumerablePropertySourceWrapper )environment.getPropertySources().get(name);
compositePropertySource.refresh();
AnoValueRefreshPostProcessor anoValueRefreshPostProcessor = SpringBeanTools.getBean(AnoValueRefreshPostProcessor.class);
List<FieldPair> list = anoValueRefreshPostProcessor.fieldPairs(key);
if (!CollectionUtils.isEmpty(list)) {
list.forEach(f -> f.updateValue(environment));
}
}catch (Exception e){
ApiLogger.bizLog.info("ApolloValueChangeEventError e: "+e);
}
}
}
5、定义Apollo监听类ApolloListener
package com.xmair.framework.apollo.listener;
import com.ctrip.framework.apollo.enums.PropertyChangeType;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import com.xmair.ecip.core.log.ApiLogger;
import com.xmair.framework.apollo.event.ApolloValueChangeEvent;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.Objects;
//监听阿波罗配置
@Data
@Configuration
public class ApolloListener {
@Autowired
private List<ApolloValueChangeEvent> apolloValueChangeEventList;
@ApolloConfigChangeListener
public void onChange(ConfigChangeEvent changeEvent) {
try{
if(!CollectionUtils.isEmpty(apolloValueChangeEventList)){
changeEvent.changedKeys().stream().map(changeEvent::getChange).forEach(change -> {
String changeKey = change.getPropertyName();
String newValue = change.getNewValue();
PropertyChangeType changeType = change.getChangeType();
for (ApolloValueChangeEvent apolloValueChangeEvent : apolloValueChangeEventList) {
if(Objects.nonNull(apolloValueChangeEvent)){
apolloValueChangeEvent.nodify(changeType,changeKey,newValue);
}
}
});
}
}catch (Exception e){
ApiLogger.bizLog.warn("apollo listener warn : "+e);
}
}
}
测试
1、测试接口
2、配置apollo
3、调用接口
4、修改apollo配置
5、再次调用测试接口,可以看到接口返回值已经成功变更