• SpringBoot项目实现配置实时刷新功能


    需求描述:在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
         */
        @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的扩展,实现实时刷新功能最核心的部分

    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.配置Bean

    @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
        }
    
    }
  • 相关阅读:
    RAID
    js 网页右下角提示框
    程序方式301
    c# ListView 虚拟模式 相关操作
    asp显示出错信息
    servu 9.3.0.1破解
    Linux下红色闪烁文件问题
    服务器实现定时开关机
    php进主页出现:HTTP 错误 500(Internal Server Error):服务器尝试执行请求时遇到了意外情况。
    怎样使用yum只下载一个包而不安装呢?
  • 原文地址:https://www.cnblogs.com/jun1019/p/11367639.html
Copyright © 2020-2023  润新知