SpringBoot中动态更新@Value配置方式
SpringBoot动态更新@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()); } }
通过测试用例,可以看到,也是可以实现我们动态替换配置的功能。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
相关文章
Java ==,equals()与hashcode()的使用
本文主要介绍了Java ==,equals()与hashcode()的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2023-05-05Idea中如何调出Run dashboard 或services窗口
这篇文章主要介绍了Idea中如何调出Run dashboard 或services窗口问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2023-03-03IDEA不识别Java文件:文件变橙色&显示后缀名.java的解决
这篇文章主要介绍了IDEA不识别Java文件:文件变橙色&显示后缀名.java的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2023-03-03
最新评论