SpringBoot項目實現配置實時刷新功能

需求描述:在SpringBoot項目中,通常業務配置都是寫死在配置文件中的,若是某個業務配置想修改,就得重啓項目。這在生產環境是不被容許的,這就須要經過技術手段作到配置變動後即便生效。下面就來看一下怎麼實現這個功能。java

來一張核心代碼截圖:數據庫

----------------------------------------------------------------------------json

實現思路:
咱們知道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.配置變動接口app

public interface DynamicConfListener {

    void onChange(String key, String value) throws Exception;

}

 

4.配置變動實現:ide

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
     */
    @Override
    public 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的擴展,實現實時刷新功能最核心的部分post

public class DynamicConfFactory extends InstantiationAwareBeanPostProcessorAdapter implements InitializingBean, DisposableBean, BeanNameAware, BeanFactoryAware {

// 注入操做配置信息的業務類 @Autowired
private SystemDictService systemDictService; @Override public void afterPropertiesSet() { DynamicConfBaseFactory.init();
// 啓動時將數據庫中的配置緩存到本地(用一個Map存) LocalDictMap.setDictMap(systemDictService.all());
} @Override public void destroy() { DynamicConfBaseFactory.destroy(); } @Override public 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; @Override public void setBeanName(String name) { this.beanName = name; } private static BeanFactory beanFactory; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } }

 

7.配置Beanthis

@Configuration
public class DynamicConfConfig {

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

}

 11.補充一個類DynamicConfBaseFactory

import com.mall.cross.dmall.base.conf.listener.DynamicConfListenerFactory;
import com.mall.cross.dmall.base.conf.listener.impl.BeanRefreshDynamicConfListener;

/**
 * dynamic config base factory
 * <p>
 * Created by shiyanjun on 2019/08/14.
 */
public class DynamicConfBaseFactory {

    public static void init() {
        DynamicConfListenerFactory.addListener(new BeanRefreshDynamicConfListener());
    }

    public static void destroy() {
        // nothing to do
    }

}
相關文章
相關標籤/搜索