SpringBoot中动态更新@Value配置方式

 更新时间:2023年09月18日 08:40:46   作者:曾小二的秃头之路  
这篇文章主要介绍了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());
    }
}

通过测试用例,可以看到,也是可以实现我们动态替换配置的功能。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java8如何使用Lambda表达式简化代码详解

    Java8如何使用Lambda表达式简化代码详解

    这篇文章主要给大家介绍了关于Java8如何使用Lambda表达式简化的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-11-11
  • 分析并发编程之LongAdder原理

    分析并发编程之LongAdder原理

    LongAdder类是JDK1.8新增的一个原子性操作类。AtomicLong通过CAS算法提供了非阻塞的原子性操作,相比受用阻塞算法的同步器来说性能已经很好了,但是JDK开发组并不满足于此,因为非常搞并发的请求下AtomicLong的性能是不能让人接受的
    2021-06-06
  • Java ==,equals()与hashcode()的使用

    Java ==,equals()与hashcode()的使用

    本文主要介绍了Java ==,equals()与hashcode()的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • Idea中如何调出Run dashboard 或services窗口

    Idea中如何调出Run dashboard 或services窗口

    这篇文章主要介绍了Idea中如何调出Run dashboard 或services窗口问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • IDEA不识别Java文件:文件变橙色&显示后缀名.java的解决

    IDEA不识别Java文件:文件变橙色&显示后缀名.java的解决

    这篇文章主要介绍了IDEA不识别Java文件:文件变橙色&显示后缀名.java的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • spring boot基于Java的容器配置讲解

    spring boot基于Java的容器配置讲解

    这篇文章主要介绍了spring boot基于Java的容器配置讲解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-04-04
  • IDEA中Maven依赖包下载不了的问题解决方案汇总

    IDEA中Maven依赖包下载不了的问题解决方案汇总

    这篇文章主要介绍了IDEA中Maven依赖包下载不了的问题解决方案汇总,文中通过图文示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-08-08
  • Java String之contains方法的使用详解

    Java String之contains方法的使用详解

    这篇文章主要介绍了Java String之contains方法的使用详解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-12-12
  • Java模拟多线程实现抢票代码实例

    Java模拟多线程实现抢票代码实例

    这篇文章主要介绍了Java模拟多线程实现抢票,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • Java ResultSet案例讲解

    Java ResultSet案例讲解

    这篇文章主要介绍了Java ResultSet案例讲解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08

最新评论