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

Posted jun1019

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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;
    

 

以上是关于SpringBoot项目实现配置实时刷新功能的主要内容,如果未能解决你的问题,请参考以下文章

springboot项目更改代码后实时刷新问题

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

Java项目:超市进销存系统设计和实现(java+Springboot+ssm+mysql+jsp+maven)

基于Springboot实现共享自习室管理系统

SpringBoot 系列教程 JPA 错误姿势之环境配置问题

SpringBoot项目结构介绍