Spring Boot 中动态更新 @Value 配置

  • Post author:
  • Post category:其他

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 版权协议,转载请附上原文出处链接和本声明。