需求描述:在SpringBoot項目中,一般業務配置都是寫死在配置文件中的,如果某個業務配置想修改,就得重啟項目。這在生產環境是不被允許的,這就需要通過技術手段做到配置變更后即使生效。下面就來看一下怎么實現這個功能。
實時刷新是什么意思、來一張核心代碼截圖:
----------------------------------------------------------------------------
實現思路:
我們知道Spring提供了@Value注解來獲取配置文件中的配置項,我們也可以自己定義一個注解來模仿Spring的這種獲取配置的方式,
只不過@Value獲取的是靜態的配置,而我們的注解要實現配置能實時刷新。比如我使用@DynamicConf("${key}")來引用配置,在SpringBoot工程啟動的時候,
就掃描項目中所有使用了該注解的Bean屬性,將配置信息從數據庫中讀取出來放到本地緩存,然后挨個賦值給加了@DynamicConf注解的屬性。
當配置有變更時,就動態給這個屬性重新賦值。這就是最核心的思路,下面看如何用代碼實現。
?
1.創建一張數據表,用于存儲配置信息:
CREATE TABLE `s_system_dict` (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵,唯一標識',`dict_name` varchar(64) NOT NULL COMMENT '字典名稱',`dict_key` varchar(255) NOT NULL COMMENT '字典KEY',`dict_value` varchar(2000) NOT NULL COMMENT '字典VALUE',`dict_type` int(11) NOT NULL DEFAULT '0' COMMENT '字典類型 0系統配置 1微信配置 2支付寶配置 3推送 4短信 5版本',`dict_desc` varchar(255) NOT NULL DEFAULT '' COMMENT '字典描述',`status` int(4) NOT NULL DEFAULT '1' COMMENT '字典狀態:0-停用 1-正常',`delete_flag` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否刪除:0-未刪除 1-已刪除',`operator` int(11) NOT NULL COMMENT '操作人ID,關聯用戶域用戶表ID',`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改時間',`delete_time` datetime NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT '刪除時間',PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8 COMMENT='配置字典表';
?
2.自定義注解
import java.lang.annotation.*; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DynamicConf {String value();String defaultValue() default "";boolean callback() default true; }
?
3.配置變更接口
public interface DynamicConfListener {void onChange(String key, String value) throws Exception;}
?
4.配置變更實現:
public class BeanRefreshDynamicConfListener implements DynamicConfListener {public static class BeanField {private String beanName;private String property;public BeanField() {}public BeanField(String beanName, String property) {this.beanName = beanName;this.property = property;}public String getBeanName() {return beanName;}public void setBeanName(String beanName) {this.beanName = beanName;}public String getProperty() {return property;}public void setProperty(String property) {this.property = property;}}private static Map<String, List<BeanField>> key2BeanField = new ConcurrentHashMap<>();public static void addBeanField(String key, BeanField beanField) {List<BeanField> beanFieldList = key2BeanField.get(key);if (beanFieldList == null) {beanFieldList = new ArrayList<>();key2BeanField.put(key, beanFieldList);}for (BeanField item : beanFieldList) {if (item.getBeanName().equals(beanField.getBeanName()) && item.getProperty().equals(beanField.getProperty())) {return; // avoid repeat refresh }}beanFieldList.add(beanField);}/*** refresh bean field** @param key* @param value* @throws Exception*/@Overridepublic void onChange(String key, String value) throws Exception {List<BeanField> beanFieldList = key2BeanField.get(key);if (beanFieldList != null && beanFieldList.size() > 0) {for (BeanField beanField : beanFieldList) {DynamicConfFactory.refreshBeanField(beanField, value, null);}}} }
?
5.用一個工程包裝一下
public class DynamicConfListenerFactory {/*** dynamic config listener repository*/private static List<DynamicConfListener> confListenerRepository = Collections.synchronizedList(new ArrayList<>());/*** add listener** @param confListener* @return*/public static boolean addListener(DynamicConfListener confListener) {if (confListener == null) {return false;}confListenerRepository.add(confListener);return true;}/*** refresh bean field** @param key* @param value*/public static void onChange(String key, String value) {if (key == null || key.trim().length() == 0) {return;}if (confListenerRepository.size() > 0) {for (DynamicConfListener confListener : confListenerRepository) {try {confListener.onChange(key, value);} catch (Exception e) {log.error(">>>>>>>>>>> refresh bean field, key={}, value={}, exception={}", key, value, e);}}}}}
?
6.對Spring的擴展,實現實時刷新功能最核心的部分
public class DynamicConfFactory extends InstantiationAwareBeanPostProcessorAdapter implements InitializingBean, DisposableBean, BeanNameAware, BeanFactoryAware {
// 注入操作配置信息的業務類@Autowiredprivate SystemDictService systemDictService;@Overridepublic void afterPropertiesSet() {DynamicConfBaseFactory.init();
// 啟動時將數據庫中的配置緩存到本地(用一個Map存)LocalDictMap.setDictMap(systemDictService.all()); }@Overridepublic void destroy() {DynamicConfBaseFactory.destroy();}@Overridepublic boolean postProcessAfterInstantiation(final Object bean, final String beanName) throws BeansException {if (!beanName.equals(this.beanName)) {ReflectionUtils.doWithFields(bean.getClass(), field -> {if (field.isAnnotationPresent(DynamicConf.class)) {String propertyName = field.getName();DynamicConf dynamicConf = field.getAnnotation(DynamicConf.class);String confKey = dynamicConf.value();confKey = confKeyParse(confKey);
// 從本地緩存中獲取配置String confValue = LocalDictMap.getDict(confKey);confValue = !StringUtils.isEmpty(confValue) ? confValue : "";BeanRefreshDynamicConfListener.BeanField beanField = new BeanRefreshDynamicConfListener.BeanField(beanName, propertyName); refreshBeanField(beanField, confValue, bean);if (dynamicConf.callback()) {BeanRefreshDynamicConfListener.addBeanField(confKey, beanField);}}});}return super.postProcessAfterInstantiation(bean, beanName);}public static void refreshBeanField(final BeanRefreshDynamicConfListener.BeanField beanField, final String value, Object bean) {if (bean == null) {try {
// 如果你的項目使用了Aop,比如AspectJ,那么有些Bean可能會被代理,
// 這里你獲取到的可能就不是真實的Bean而是被代理后的Bean,所以這里獲取真實的Bean;bean = AopTargetUtils.getTarget(DynamicConfFactory.beanFactory.getBean(beanField.getBeanName()));} catch (Exception e) {log.error(">>>>>>>>>>>> Get target bean fail!!!!!");}}if (bean == null) {return;}BeanWrapper beanWrapper = new BeanWrapperImpl(bean);PropertyDescriptor propertyDescriptor = null;PropertyDescriptor[] propertyDescriptors = beanWrapper.getPropertyDescriptors();if (propertyDescriptors != null && propertyDescriptors.length > 0) {for (PropertyDescriptor item : propertyDescriptors) {if (beanField.getProperty().equals(item.getName())) {propertyDescriptor = item;}}}if (propertyDescriptor != null && propertyDescriptor.getWriteMethod() != null) {beanWrapper.setPropertyValue(beanField.getProperty(), value);log.info(">>>>>>>>>>> refresh bean field[set] success, {}#{}={}", beanField.getBeanName(), beanField.getProperty(), value);} else {final Object finalBean = bean;ReflectionUtils.doWithFields(bean.getClass(), fieldItem -> {if (beanField.getProperty().equals(fieldItem.getName())) {try {Object valueObj = FieldReflectionUtil.parseValue(fieldItem.getType(), value);fieldItem.setAccessible(true);fieldItem.set(finalBean, valueObj);log.info(">>>>>>>>>>> refresh bean field[field] success, {}#{}={}", beanField.getBeanName(), beanField.getProperty(), value);} catch (IllegalAccessException e) {throw new RuntimeException(">>>>>>>>>>> refresh bean field[field] fail, " + beanField.getBeanName() + "#" + beanField.getProperty() + "=" + value);}}});}}private static final String placeholderPrefix = "${";private static final String placeholderSuffix = "}";/*** valid placeholder** @param originKey* @return*/private static boolean confKeyValid(String originKey) {if (originKey == null || "".equals(originKey.trim())) {throw new RuntimeException(">>>>>>>>>>> originKey[" + originKey + "] not be empty");}boolean start = originKey.startsWith(placeholderPrefix);boolean end = originKey.endsWith(placeholderSuffix);return start && end ? true : false;}/*** parse placeholder** @param originKey* @return*/private static String confKeyParse(String originKey) {if (confKeyValid(originKey)) {return originKey.substring(placeholderPrefix.length(), originKey.length() - placeholderSuffix.length());}return originKey;}private String beanName;@Overridepublic void setBeanName(String name) {this.beanName = name;}private static BeanFactory beanFactory;@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {this.beanFactory = beanFactory;}}
?
7.配置Bean
@Configuration public class DynamicConfConfig {@Beanpublic DynamicConfFactory dynamicConfFactory() {DynamicConfFactory dynamicConfFactory = new DynamicConfFactory(); return dynamicConfFactory;}}
?
8.使用方式
@RestController @RequestMapping("/test") public class TestController {@DynamicConf("${test.dynamic.config.key}")private String testDynamicConfig; @GetMapping("/getConfig")public JSONObject testDynamicConfig(String key) {
// 從本地緩存獲取配置(就是一個Map)String value = LocalDictMap.getDict(key);JSONObject json = new JSONObject();json.put(key, value);return json;}
// 通過接口來修改數據庫中的配置信息@GetMapping("/updateConfig")public String updateConfig(String key, String value) {SystemDictDto dictDto = new SystemDictDto();dictDto.setDictKey(key);dictDto.setDictValue(value);systemDictService.update(dictDto, 0);return "success";} }
?
9.配置變更后刷新
// 刷新Bean屬性
DynamicConfListenerFactory.onChange(dictKey, dictValue);
// TODO 刷新本地緩存 略
?
10.補上一個工具類)
public class AopTargetUtils {/*** 獲取目標對象** @param proxy 代理對象* @return 目標對象* @throws Exception*/public static Object getTarget(Object proxy) throws Exception {if (!AopUtils.isAopProxy(proxy)) {return proxy;}if (AopUtils.isJdkDynamicProxy(proxy)) {proxy = getJdkDynamicProxyTargetObject(proxy);} else {proxy = getCglibProxyTargetObject(proxy);}return getTarget(proxy);}private static Object getCglibProxyTargetObject(Object proxy) throws Exception {Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");h.setAccessible(true);Object dynamicAdvisedInterceptor = h.get(proxy);Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");advised.setAccessible(true);Object target = ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();return target;}private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {Field h = proxy.getClass().getSuperclass().getDeclaredField("h");h.setAccessible(true);AopProxy aopProxy = (AopProxy) h.get(proxy);Field advised = aopProxy.getClass().getDeclaredField("advised");advised.setAccessible(true);Object target = ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();return target;}}
?