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;
    }

}

原文地址:https://www.cnblogs.com/jun1019/p/11367639.html

时间: 2024-10-09 00:54:33

SpringBoot项目实现配置实时刷新功能的相关文章

springboot项目中配置self4j

首先,声明一下,springboot项目中默认情况下已经集成了self4J + LogBack. slf4j作为一个接口定义,底层可以有很多实现框架,同时也可以支持别的日志实现或者框架打到sfl4j上.它的实现是基于不同的桥接包.slf4j作为接口定义,下面有很多种实现.实现原理是获取ILoggerFactory时执行初始化,初始化过程绑定实现对象:load出所有实现StaticLoggerBinder的类,然后获取他的单例,后面执行getLogger的时候都是调用这个单例类的方法获取对应有具体

springboot项目接入配置中心,实现@ConfigurationProperties的bean属性刷新方案

前言 配置中心,通过key=value的形式存储环境变量.配置中心的属性做了修改,项目中可以通过配置中心的依赖(sdk)立即感知到.需要做的就是如何在属性发生变化时,改变带有@ConfigurationProperties的bean的相关属性. 配置中心 在读配置中心源码的时候发现,里面维护了一个Environment,以及ZookeeperPropertySource.当配置中心属性发生变化的时候,清空ZookeeperPropertySource,并放入最新的属性值. public clas

SpringBoot项目属性配置

application.properties和application.yml进行配置 application.yml -- 比较方便和直观,不用写全 server: port: 8080 context-path: /sboot application.properties -- 不太直观,比较老的配置文件会使用这个 server.port = 8080 server.context-path= /sboot 常用注解 @Value : 获取properties\yml文件中的配置值,简化了读取

springboot项目maven配置多环境

maven配置多环境 <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <!-- 配置默认跳过单元测试 结束--> <plugin> <groupId>org.apa

idea内springboot项目设置热部署

一.需求分析: 编写idea中编写项目时,经常性改某几行代码就需要重新启动项目,比较浪费时间,借助idea的热部署可以实现代码的热部署 二.实现经过 这边可以借助spring-boot-devtools模块进行配置,devtools会检测代码,并进行重新发布.devtools的实现原理是通过使用两个 ClassLoader,一个用来加载一些第三方的代码(如引入的一些jar包).另一个ClassCLoud会加载一些会更改的代码,可以称 为restart ClassLoader.在有代码进行更改的时

Springboot项目配置druid数据库连接池,并监控统计功能

pom.xml配置依赖 <!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.6</version> </dependency>  资源文件配置信息 不管是

IDEA 配置maven + SpringBoot项目在新电脑上的配置运行

该教程记录了我在一台新的电脑上安装IDEA,配置JAVA+MAVEN+GIT+SpringBoot项目的过程,最终完成了项目的运行. 一.若想利用IDEA的git工具从GitHub或者码云上面获取项目,需要提前下载git软件,并完成初步的配置. 1. git的下载和配置  进入git官网,https://www.git-scm.com/downloads并下载. 确定自己要下载的版本(我下载的是64bit版本的windows安装程序),点击下载即可. 下载完之后,双击应用程序,一路next即可完

springboot项目启动之后初始化自定义配置类

前言 今天在写项目的时候,需要再springboot项目启动之后,加载我自定义的配置类的一些方法,百度了之后特此记录下. 正文 方法有两种: 1. 创建自定义类实现 CommandLineRunner接口,重写run()方法.springboot启动之后会默认去扫描所有实现了CommandLineRunner的类,并运行其run()方法. @Component @Order(2) //通过order值的大小来决定启动的顺序 public class AskForLeave implements

SrpingCloud 之SrpingCloud config分布式配置中心实时刷新

默认情况下是不能及时获取变更的配置文件信息 Spring Cloud分布式配置中心可以采用手动或者自动刷新 1.手动需要人工调用接口   监控中心 2.消息总线实时通知  springbus 动态刷新数据 在SpringCloud中有手动刷新配置文件和实时刷新配置文件两种方式. 手动方式采用actuator端点刷新数据 实时刷新采用SpringCloud Bus消息总线 actuator端点刷新数据 在config clientr引入 <dependency> <groupId>o