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 '' COMMENT '字典类型 0系统配置 1微信配置 2支付宝配置 3推送 4短信 5版本',
`dict_desc` varchar(255) NOT NULL DEFAULT '' COMMENT '字典描述',
`status` int(4) NOT NULL DEFAULT '' COMMENT '字典状态:0-停用 1-正常',
`delete_flag` tinyint(1) NOT NULL DEFAULT '' 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
} }
SpringBoot项目实现配置实时刷新功能的更多相关文章
- Spring-Boot项目中配置redis注解缓存
Spring-Boot项目中配置redis注解缓存 在pom中添加redis缓存支持依赖 <dependency> <groupId>org.springframework.b ...
- Spring Cloud实战之初级入门(五)— 配置中心服务化与配置实时刷新
目录 1.环境介绍 2.配置中心服务化 2.1 改造mirco-service-spring-config 2.2 改造mirco-service-provider.mirco-service-con ...
- SpringBoot项目属性配置
如果使用IDEA创建Springboot项目,默认会在resource目录下创建application.properties文件,在SpringBoot项目中,也可以使用yml类型的配置文件代替pro ...
- springboot项目属性配置及注意事项
在idea编辑器建的springboot项目中的resources包下的application.properties这个就是配置文件. 另外配置文件的文件名还可以是application.yml,在r ...
- springboot项目接入配置中心,实现@ConfigurationProperties的bean属性刷新方案
前言 配置中心,通过key=value的形式存储环境变量.配置中心的属性做了修改,项目中可以通过配置中心的依赖(sdk)立即感知到.需要做的就是如何在属性发生变化时,改变带有@Configuratio ...
- SpringBoot项目属性配置-第二章
SpringBoot入门 1. 相信很多人选择Spring Boot主要是考虑到它既能兼顾Spring的强大功能,还能实现快速开发的便捷.我们在Spring Boot使用过程中,最直观的感受就是没有了 ...
- 基于springBoot项目如何配置多数据源
前言 有时,在一个项目中会用到多数据源,现在对自己在项目中多数据源的操作总结如下,有不到之处敬请批评指正! 1.pom.xml的依赖引入 <dependency> <groupId& ...
- springboot项目中配置swagger-ui
Git官方地址:https://github.com/SpringForAll/spring-boot-starter-swagger Demo:https://github.com/dyc87112 ...
- spring boot学习(2) SpringBoot 项目属性配置
第一节:项目内置属性 application.properties配置整个项目的,相当于以前的web.xml: 注意到上一节的访问HelloWorld时,项目路径也没有加:直接是http://loca ...
随机推荐
- thrift中的概念
Thrift的网络栈 Apache Thrift的网络栈的简单表示如下: +-------------------------------------------+ | Server | | (sin ...
- 【Mybatis异常】 org.apache.ibatis.binding.BindingException: Parameter 'storeId' not found. Available parameters are [form, param1]
一.异常信息 2019-05-31 16:06:25.272 [http-nio-10650-exec-3] WARN o.s.w.s.m.m.a.ExceptionHandlerExceptionR ...
- MySQL/MariaDB数据库的函数
MySQL/MariaDB数据库的函数 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. MySQL/MariaDB数据库的函数分为系统函数和用户自定义函数(user-define ...
- 攻防世界WEB高手进阶之Zhuanxv
1.一开始就是一个时钟界面 2.扫描目录发现/list 目录 打开是后台登陆,看了一下源码,也没发现什么,焦灼... 3.百度上搜了一波wp,发现原来在css里面藏了东西 后台的背景图片居然是这样读取 ...
- HDU4548美素数——筛选法与空间换时间
对于数论的学习比较的碎片化,所以开了一篇随笔来记录一下学习中遇到的一些坑,主要通过题目来讲解 本题围绕:素数筛选法与空间换时间 HDU4548美素数 题目描述 小明对数的研究比较热爱,一谈到数,脑子里 ...
- 异常错误:在可以调用 OLE 之前,必须将当前线程设置为单线程单元(STA)模式
最近做一个蛋疼的东西就是C#调用windows API 来操作一个摄像头,自动处理一些东西.要用到剪切板复制 粘贴功能,即 Clipboard.SetDataObject(filedic, true) ...
- SD介绍
1. 介绍 MMC,MultiMediaCard,即多媒体卡,是一种非易失性存储器件,有7pin,目前已基本被SD卡代替 eMMC,Embedded Multimedia Card,内嵌式存储器,以B ...
- ZOJ - 2132:The Most Frequent Number(思维题)
pro:给定N个数的数组a[],其中一个数X的出现次数大于N/2,求X,空间很小. sol:不能用保存数组,考虑其他做法. 由于出现次数较多,我们维护一个栈,栈中的数字相同,所以我们记录栈的元素和个数 ...
- B-树,B+树,B*树总结
链接地址:https://blog.csdn.net/v_JULY_v/article/details/6530142 B+树 B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一棵B+ ...
- MySql添加字段命令
使用ALTER TABLE命令来向一个表添加字段,示例如下: -- 向t_user表添加user_age字段 ) DEFAULT NULL COMMENT '年龄' AFTER user_email; ...