当前位置:   article > 正文

Spring Boot 中动态更新 @Value 配置_spring @value 动态改变值

spring @value 动态改变值

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);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

然后我们需要个注册表,来保存 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));
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

当然,我们还需要一个工具类,来解析占位符。

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());
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133

接着,我们就可以依赖于 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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

至此,我们就已经实现了 @Value 注解动态更新的主要逻辑了,我们通过一个测试用例来看一下效果。

2.2 测试用例

我们在 resources 目录下,创建一个配置文件 application-dynamic.properties

zzn.dynamic.name=default-name
  • 1

然后新建配置文件对应的配置类

@Component
@Getter
@PropertySource(value={"classpath:application-dynamic.properties"})
public class DynamicProperties {

    @Value("${zzn.dynamic.name}")
    private String dynamicName;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

测试方法如下:

@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());
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50

然后,将我们自定义的 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;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

定义一个注解,方便我们快速使用

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope(BeanRefreshScope.SCOPE_REFRESH)
@Documented
public @interface RefreshScope {

    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

定义一个工具类,实现配置的替换。

@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;
    }


}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

接下来,我们使用测试用例来验证一下。

3.2 测试用例

我们同上一个用例一样,在 resources 目录下,创建一个配置文件 application-dynamic.properties

zzn.dynamic.name=default-name
  • 1

然后新建配置文件对应的配置类,区别在于这个配置文件上面加了 @RefreshScope 注解

@RefreshScope
@Component
@Getter
@PropertySource(value={"classpath:application-dynamic.properties"})
public class DynamicProperties {

    @Value("${zzn.dynamic.name}")
    private String dynamicName;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

测试方法如下:

@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());
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/262539?site
推荐阅读
相关标签
  

闽ICP备14008679号