Spring Boot 中动态更新 @Value 配置
1 背景
通常我们在项目运行过程中,会有修改配置的需求,但是在没有接入分布式配置中心的情况下,经常修改一个配置就需要重启一次容器,但是项目的重启时间久,而且重启还会影响用户的使用,因此需要在不重启的情况下,动态修改配置。我们可以通过以下两种方式,实现 @Value 配置的动态更新。
2 通过反射实现 @Value 配置的更新
2.1 代码实现
首先,我们需要创建一个对象,来保存 @Value 的所有信息。
@Getter
public class SpringValue {
/**
* bean 的弱引用
*/
private final WeakReference<Object> beanRef;
/**
* bean 名称
*/
private final String beanName;
/**
* 字段
*/
private final Field field;
/**
* 属性的键
*/
private final String key;
/**
* 对应的占位符
*/
private final String placeholder;
/**
* 字段的类对象
*/
private final Class<?> targetType;
public SpringValue(String key, String placeholder, Object bean, String beanName, Field field) {
this.beanRef = new WeakReference<>(bean);
this.beanName = beanName;
this.field = field;
this.key = key;
this.placeholder = placeholder;
this.targetType = field.getType();
}
@SneakyThrows
public void update(Object newVal) {
injectField(newVal);
}
/**
* 使用反射,给字段注入新的值
*
* @param newVal 新的值
* @throws IllegalAccessException 发送反射异常时
*/
private void injectField(Object newVal) throws IllegalAccessException {
Object bean = beanRef.get();
if (bean == null) {
return;
}
boolean accessible = field.isAccessible();
field.setAccessible(true);
field.set(bean, newVal);
field.setAccessible(accessible);
}
}
然后我们需要个注册表,来保存 key 和 SpringValue 的映射关系。因为一个 key 有可能对应多个 SpringValue,所以这里使用 Multimap。
public class SpringValueRegistry {
private static final SpringValueRegistry INSTANCE = new SpringValueRegistry();
private final Multimap<String, SpringValue> registry = LinkedListMultimap.create();
private final Object lock = new Object();
private SpringValueRegistry() {
}
public static SpringValueRegistry getInstance() {
return INSTANCE;
}
public void register(String key, SpringValue springValue) {
synchronized (lock) {
registry.put(key, springValue);
}
}
public Collection<SpringValue> get(String key) {
return registry.get(key);
}
public void updateValue(String key, Object newValue) {
get(key).forEach(springValue -> springValue.update(newValue));
}
}
当然,我们还需要一个工具类,来解析占位符。
public class PlaceholderHelper {
private static final String PLACEHOLDER_PREFIX = "${";
private static final String PLACEHOLDER_SUFFIX = "}";
private static final String VALUE_SEPARATOR = ":";
private static final String SIMPLE_PLACEHOLDER_PREFIX = "{";
private static final String EXPRESSION_PREFIX = "#{";
private static final String EXPRESSION_SUFFIX = "}";
private static final PlaceholderHelper INSTANCE = new PlaceholderHelper();
private PlaceholderHelper() {
}
public static PlaceholderHelper getInstance() {
return INSTANCE;
}
/**
* 解析占位符
*
* @param propertyString 占位符字符串
* @return 获取键
*/
public Set<String> extractPlaceholderKeys(String propertyString) {
Set<String> placeholderKeys = new HashSet<>();
if (!StringUtils.hasText(propertyString) ||
(!isNormalizedPlaceholder(propertyString) &&
!isExpressionWithPlaceholder(propertyString))) {
return placeholderKeys;
}
Deque<String> stack = new LinkedList<>();
stack.push(propertyString);
while (!stack.isEmpty()) {
String strVal = stack.pop();
int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX);
if (startIndex == -1) {
placeholderKeys.add(strVal);
continue;
}
int endIndex = findPlaceholderEndIndex(strVal, startIndex);
if (endIndex == -1) {
// 找不到占位符
continue;
}
String placeholderCandidate = strVal.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
// 处理 ${some.key:other.key}
if (placeholderCandidate.startsWith(PLACEHOLDER_PREFIX)) {
stack.push(placeholderCandidate);
} else {
// 处理 some.key:${some.other.key:100}
int separatorIndex = placeholderCandidate.indexOf(VALUE_SEPARATOR);
if (separatorIndex == -1) {
stack.push(placeholderCandidate);
} else {
stack.push(placeholderCandidate.substring(0, separatorIndex));
String defaultValuePart =
normalizeToPlaceholder(placeholderCandidate.substring(separatorIndex + VALUE_SEPARATOR.length()));
if (StringUtils.hasText(defaultValuePart)) {
stack.push(defaultValuePart);
}
}
}
// 有剩余部分,例如: ${a}.${b}
if (endIndex + PLACEHOLDER_SUFFIX.length() < strVal.length() - 1) {
String remainingPart = normalizeToPlaceholder(strVal.substring(endIndex + PLACEHOLDER_SUFFIX.length()));
if (StringUtils.hasText(remainingPart)) {
stack.push(remainingPart);
}
}
}
return placeholderKeys;
}
/**
* 判断是不是标准的占位符,即以 '${' 开头,并且包含 '}'
*
* @param propertyString 属性字符串
* @return 如果是标准的占位符,则返回 true
*/
private boolean isNormalizedPlaceholder(String propertyString) {
return propertyString.startsWith(PLACEHOLDER_PREFIX) && propertyString.contains(PLACEHOLDER_SUFFIX);
}
private boolean isExpressionWithPlaceholder(String propertyString) {
return propertyString.startsWith(EXPRESSION_PREFIX) && propertyString.contains(EXPRESSION_SUFFIX)
&& propertyString.contains(PLACEHOLDER_PREFIX) && propertyString.contains(PLACEHOLDER_SUFFIX);
}
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
int index = startIndex + PLACEHOLDER_PREFIX.length();
int withinNestedPlaceholder = 0;
while (index < buf.length()) {
if (StringUtils.substringMatch(buf, index, PLACEHOLDER_SUFFIX)) {
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + PLACEHOLDER_SUFFIX.length();
} else {
return index;
}
} else if (StringUtils.substringMatch(buf, index, SIMPLE_PLACEHOLDER_PREFIX)) {
withinNestedPlaceholder++;
index = index + SIMPLE_PLACEHOLDER_PREFIX.length();
} else {
index++;
}
}
return -1;
}
private String normalizeToPlaceholder(String strVal) {
int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX);
if (startIndex == -1) {
return null;
}
int endIndex = strVal.lastIndexOf(PLACEHOLDER_SUFFIX);
if (endIndex == -1) {
return null;
}
return strVal.substring(startIndex, endIndex + PLACEHOLDER_SUFFIX.length());
}
}
接着,我们就可以依赖于 spring boot 的生命周期,继承 BeanPostProcessor,来处理 @Value 注解的值,将其注册到注册表中。
@Slf4j
@Component
public class SpringValueProcessor implements BeanPostProcessor, PriorityOrdered {
private final SpringValueRegistry springValueRegistry;
private final PlaceholderHelper placeholderHelper;
public SpringValueProcessor() {
this.springValueRegistry = SpringValueRegistry.getInstance();
this.placeholderHelper = PlaceholderHelper.getInstance();
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class<?> clazz = bean.getClass();
for (Field field : findAllField(clazz)) {
processField(bean, beanName, field);
}
return bean;
}
private void processField(Object bean, String beanName, Field field) {
// 查找有 @Value 注释的字段
Value value = field.getAnnotation(Value.class);
if (value == null) {
return;
}
doRegister(bean, beanName, field, value);
}
private void doRegister(Object bean, String beanName, Field field, Value value) {
Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
if (keys.isEmpty()) {
return;
}
for (String key : keys) {
SpringValue springValue;
springValue = new SpringValue(key, value.value(), bean, beanName, field);
springValueRegistry.register(key, springValue);
log.info("Monitoring {}", springValue);
}
}
@Override
public int getOrder() {
// 设置为最低优先级
return Ordered.LOWEST_PRECEDENCE;
}
private List<Field> findAllField(Class<?> clazz) {
final List<Field> res = new LinkedList<>();
ReflectionUtils.doWithFields(clazz, res::add);
return res;
}
}
至此,我们就已经实现了 @Value 注解动态更新的主要逻辑了,我们通过一个测试用例来看一下效果。
2.2 测试用例
我们在 resources 目录下,创建一个配置文件 application-dynamic.properties
。
zzn.dynamic.name=default-name
然后新建配置文件对应的配置类
@Component
@Getter
@PropertySource(value={"classpath:application-dynamic.properties"})
public class DynamicProperties {
@Value("${zzn.dynamic.name}")
private String dynamicName;
}
测试方法如下:
@SpringBootTest
class SpringValueApplicationTests {
@Autowired
private DynamicProperties dynamicProperties;
@Test
void testDynamicUpdateValue() {
Assertions.assertEquals("default-name", dynamicProperties.getDynamicName());
SpringValueRegistry.getInstance().updateValue("zzn.dynamic.name", "dynamic-name");
Assertions.assertEquals("dynamic-name", dynamicProperties.getDynamicName());
}
}
3 通过 Scope 实现 @Value 配置的更新
3.1 代码实现
首先,我们可以继承 Scope 接口,实现我们自定义的 Scope。
@Slf4j
public class BeanRefreshScope implements Scope {
public static final String SCOPE_REFRESH = "refresh";
private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();
/**
* 使用 Map 缓存 bean 实例
*/
private final ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>();
private BeanRefreshScope() {
}
public static BeanRefreshScope getInstance() {
return INSTANCE;
}
/**
* 清理 bean 缓存
*/
public static void clear() {
INSTANCE.beanMap.clear();
}
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
log.info("BeanRefreshScope, get bean name: {}", name);
return beanMap.computeIfAbsent(name, s -> objectFactory.getObject());
}
@Override
public Object remove(String name) {
return beanMap.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return null;
}
}
然后,将我们自定义的 scope 注入到 spring context 中。
@Configuration
public class ScopeConfig {
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer customScopeConfigurer = new CustomScopeConfigurer();
Map<String, Object> map = new HashMap<>();
map.put(BeanRefreshScope.SCOPE_REFRESH, BeanRefreshScope.getInstance());
// 配置 scope
customScopeConfigurer.setScopes(map);
return customScopeConfigurer;
}
}
定义一个注解,方便我们快速使用
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope(BeanRefreshScope.SCOPE_REFRESH)
@Documented
public @interface RefreshScope {
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
定义一个工具类,实现配置的替换。
@Component
public class RefreshConfigUtil implements EnvironmentAware {
private static ConfigurableEnvironment environment;
public static void updateValue(String key, Object newValue) {
// 自定义的配置文件名称
MutablePropertySources propertySources = environment.getPropertySources();
propertySources.stream()
.forEach(x -> {
if (x instanceof MapPropertySource) {
MapPropertySource propertySource = (MapPropertySource) x;
if (propertySource.containsProperty(key)) {
String name = propertySource.getName();
Map<String, Object> source = propertySource.getSource();
Map<String, Object> map = new HashMap<>(source.size());
map.putAll(source);
map.put(key, newValue);
environment.getPropertySources().replace(name, new MapPropertySource(name, map));
}
}
});
// 刷新缓存
BeanRefreshScope.clear();
}
@Override
public void setEnvironment(Environment environment) {
RefreshConfigUtil.environment = (ConfigurableEnvironment) environment;
}
}
接下来,我们使用测试用例来验证一下。
3.2 测试用例
我们同上一个用例一样,在 resources 目录下,创建一个配置文件 application-dynamic.properties
。
zzn.dynamic.name=default-name
然后新建配置文件对应的配置类,区别在于这个配置文件上面加了 @RefreshScope
注解
@RefreshScope
@Component
@Getter
@PropertySource(value={"classpath:application-dynamic.properties"})
public class DynamicProperties {
@Value("${zzn.dynamic.name}")
private String dynamicName;
}
测试方法如下:
@SpringBootTest
class SpringValueApplicationTests {
@Autowired
private DynamicProperties dynamicProperties;
@Test
void testDynamicUpdateValue() {
Assertions.assertEquals("default-name", dynamicProperties.getDynamicName());
RefreshConfigUtil.updateValue("zzn.dynamic.name", "dynamic-name");
Assertions.assertEquals("dynamic-name", dynamicProperties.getDynamicName());
}
}
通过测试用例,可以看到,也是可以实现我们动态替换配置的功能。
版权声明:本文为qq_40161813原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。