SpringBoot中動態(tài)更新@Value配置方式
SpringBoot動態(tài)更新@Value配置
1 背景
通常我們在項目運行過程中,會有修改配置的需求,但是在沒有接入分布式配置中心的情況下,經(jīng)常修改一個配置就需要重啟一次容器,但是項目的重啟時間久,而且重啟還會影響用戶的使用,因此需要在不重啟的情況下,動態(tài)修改配置。
我們可以通過以下兩種方式,實現(xiàn) @Value 配置的動態(tài)更新。
2 通過反射實現(xiàn)@Value配置的更新
2.1 代碼實現(xiàn)
首先,我們需要創(chuàng)建一個對象,來保存 @Value 的所有信息。
@Getter
public class SpringValue {
/**
* bean 的弱引用
*/
private final WeakReference<Object> beanRef;
/**
* bean 名稱
*/
private final String beanName;
/**
* 字段
*/
private final Field field;
/**
* 屬性的鍵
*/
private final String key;
/**
* 對應(yīng)的占位符
*/
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 發(fā)送反射異常時
*/
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 的映射關(guān)系。
因為一個 key 有可能對應(yīng)多個 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));
}
}當(dāng)然,我們還需要一個工具類,來解析占位符。
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}.$
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;
}
/**
* 判斷是不是標(biāo)準(zhǔn)的占位符,即以 '${' 開頭,并且包含 '}'
*
* @param propertyString 屬性字符串
* @return 如果是標(biāo)準(zhǔn)的占位符,則返回 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() {
// 設(shè)置為最低優(yōu)先級
return Ordered.LOWEST_PRECEDENCE;
}
private List<Field> findAllField(Class<?> clazz) {
final List<Field> res = new LinkedList<>();
ReflectionUtils.doWithFields(clazz, res::add);
return res;
}
}至此,我們就已經(jīng)實現(xiàn)了 @Value 注解動態(tài)更新的主要邏輯了,我們通過一個測試用例來看一下效果。
2.2 測試用例
我們在 resources 目錄下,創(chuàng)建一個配置文件 application-dynamic.properties 。
zzn.dynamic.name=default-name
然后新建配置文件對應(yīng)的配置類
@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實現(xiàn)@Value配置的更新
3.1 代碼實現(xiàn)
首先,我們可以繼承 Scope 接口,實現(xiàn)我們自定義的 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;
}定義一個工具類,實現(xiàn)配置的替換。
@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 目錄下,創(chuàng)建一個配置文件 application-dynamic.properties 。
zzn.dynamic.name=default-name
然后新建配置文件對應(yīng)的配置類,區(qū)別在于這個配置文件上面加了 @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());
}
}通過測試用例,可以看到,也是可以實現(xiàn)我們動態(tài)替換配置的功能。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
springmvc處理模型數(shù)據(jù)ModelAndView過程詳解
這篇文章主要介紹了springmvc處理模型數(shù)據(jù)ModelAndView過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-01-01
springboot整合shiro多驗證登錄功能的實現(xiàn)(賬號密碼登錄和使用手機驗證碼登錄)
這篇文章給大家介紹springboot整合shiro多驗證登錄功能的實現(xiàn)方法,包括賬號密碼登錄和使用手機驗證碼登錄功能,本文通過實例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2021-07-07
Springboot2.x+Quartz分布式集群的實現(xiàn)
這篇文章主要介紹了Springboot2.x+Quartz分布式集群的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
Java實現(xiàn)簡單的銀行管理系統(tǒng)的示例代碼
這篇文章主要介紹了如何利用Java實現(xiàn)簡單的銀行管理系統(tǒng),可以實現(xiàn)存款,取款,查詢等功能,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-09-09
Java cglib為實體類(javabean)動態(tài)添加屬性方式
這篇文章主要介紹了Java cglib為實體類(javabean)動態(tài)添加屬性方式,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02

