Springboot动态修改配置@Value 可以分布式部署
深圳搬运工
开发背景
由于服务里的策略修改太频繁,导致需要频繁同步更新并且部署,所以希望通过配置动态化实现页面无感知的修改.满足于个个场景的需求 (当然最好接入携程的Apollo) 这个我只是不想接第三方服务
开发逻辑
- 首先获取@value的所有配置项,并且过滤到频繁修改配置项,保存到集合里面.
(判断是否有自定义的@springvalue注解,然后进行组装数据到集合里面,通过反射来修改运行值)
—实现 BeanPostProcessor: bean对象初始化 相关操作
- 页面修改的时候,通过Redis发布订阅实现集群之间无差别同步修改.保证个个服务的值都是相同的.
(修改后,需要发布,才能直接更新到运行时的类,重启服务也是可以自动加载数据库的最新数据值)
开发详情
1. 自定义注解@StringValue
该注解适用于需要动态配置的相关配置类
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//需要动态修改配置的类 则用上这个注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SpringValue {
}
2. 实现BeanPostProcessor的2个方法
第一步:
Bean初始化前postProcessBeforeInitialization:
通过反射获取到类上面有自定义注解的@SpringValue配置类
把key和value保存到springValueCacheMap集合里面
Key:当前类的完整标准名称
Value:组装当前class的信息到SpringValue实体中
第二步:
Bean初始化后postProcessAfterInitialization:
通过反射获取到类上面有自定义注解的@SpringValue配置类
根据当前类的完成标准名称为条件查询数据库满足的配置项
根据查询出来的实体类的key 来匹配 当前类的完整标准名称
然后通过反射来暴力修改当前的字段值(初始化查库修改)
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
import com.zhuiyi.yicall.callout.request.SyncValueQueryRest;
import com.zhuiyi.yicall.common.SpringValue;
import com.zhuiyi.yicall.dao.SyncValueConfigDao;
import com.zhuiyi.yicall.po.SyncValueConfigPO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@Slf4j
@Configuration
public class SpringValueProcessor implements BeanPostProcessor {
//占位符相关操作
private final PlaceholderHelper placeholderHelper = new PlaceholderHelper();
//cache key:类的完成标准的名称 value: class,field,...等等相关信息
public static final Multimap<String, SpringValue> springValueCacheMap = LinkedListMultimap.create();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
//首先判断是否有这个注解
if (bean.getClass().isAnnotationPresent(com.zhuiyi.yicall.constant.annotation.SpringValue.class)) {
log.info("-----------> 加载的所有系统的配置:【{}】 <----------",beanName);
//获取class的对象
Class obj = bean.getClass();
//查询全部字段
List<Field> fields = findAllField(obj);
//遍历所有的字段值
for (Field field : fields) {
//获取字段上的注解
Value value = field.getAnnotation(Value.class);
if (value != null) {
//去除特殊符号
Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
for (String key : keys) {
//key=yicall-sync.callBackUrl.act value.value=${yicall-sync.callBackUrl.act}
SpringValue springValue = new SpringValue(key, value.value(), bean, obj.getName(),obj.getSimpleName(), field, false);
//放在map里面 key=yicall-sync.callBackUrl.act value=这个值的详情
springValueCacheMap.put(key, springValue);
}
}
}
}
return bean;
}
//查询全部数据库需要配置的属性
@Autowired
private SyncValueConfigDao syncValueConfigDao;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean.getClass().isAnnotationPresent(com.zhuiyi.yicall.constant.annotation.SpringValue.class)) {
SyncValueQueryRest rest = new SyncValueQueryRest();
rest.setSyncClass(bean.getClass().getName());
//查询全部keyValue值
List<SyncValueConfigPO> pos = syncValueConfigDao.selectAll(rest);
log.info("查询出来的数据库【{}】配置项:大小:【{}】",beanName, pos.size());
if (!pos.isEmpty()) {
for (SyncValueConfigPO po : pos) {
String key = po.getSyncKey();
Collection<SpringValue> values = springValueCacheMap.get(key);
for (SpringValue value : values) {
try {
log.info("beanSimpleName{}.key:【{}】old值:【{}】new值:【{}】",value.getBeanSimpleName(), key, value.getFieldValue(), po.getSyncValue());
value.update(po.getSyncValue());
} catch (IllegalAccessException e) {
log.info("key:{} , value:{} class:{} ,修改报错:IllegalAccessException", key, po.getSyncValue(), po.getSyncClass());
} catch (InvocationTargetException e) {
log.info("key:{} , value:{} class:{} ,修改报错:InvocationTargetException", key, po.getSyncValue(), po.getSyncClass());
}
}
}
}
}
return bean;
}
//获取所有的字段属性
private List<Field> findAllField(Class clazz) {
final List<Field> res = new LinkedList<>();
ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException {
res.add(field);
}
});
return res;
}
}
占位符相关处理类
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanExpressionContext;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.util.StringUtils;
import java.util.Objects;
import java.util.Set;
import java.util.Stack;
/**
* Placeholder helper functions.
* 占位符相关操作 切割
*/
public class PlaceholderHelper {
private static final String PLACEHOLDER_PREFIX = "${";
private static final String PLACEHOLDER_SUFFIX = "}";
private static final String VALUE_SEPARATOR = ":";
private static final String SIMPLE_PLACEHOLDER_PREFIX = "{";
private static final String EXPRESSION_PREFIX = "#{";
private static final String EXPRESSION_SUFFIX = "}";
/**
* Resolve placeholder property values, e.g.
* <br />
* <br />
* "${somePropertyValue}" -> "the actual property value"
*/
public Object resolvePropertyValue(ConfigurableBeanFactory beanFactory, String beanName, String placeholder) {
// resolve string value
String strVal = beanFactory.resolveEmbeddedValue(placeholder);
BeanDefinition bd = (beanFactory.containsBean(beanName) ? beanFactory
.getMergedBeanDefinition(beanName) : null);
// resolve expressions like "#{systemProperties.myProp}"
return evaluateBeanDefinitionString(beanFactory, strVal, bd);
}
private Object evaluateBeanDefinitionString(ConfigurableBeanFactory beanFactory, String value,
BeanDefinition beanDefinition) {
if (beanFactory.getBeanExpressionResolver() == null) {
return value;
}
Scope scope = (beanDefinition != null ? beanFactory
.getRegisteredScope(Objects.requireNonNull(beanDefinition.getScope())) : null);
return beanFactory.getBeanExpressionResolver()
.evaluate(value, new BeanExpressionContext(beanFactory, scope));
}
/**
* Extract keys from placeholder, e.g.
* <ul>
* <li>${some.key} => "some.key"</li>
* <li>${some.key:${some.other.key:100}} => "some.key", "some.other.key"</li>
* <li>${${some.key}} => "some.key"</li>
* <li>${${some.key:other.key}} => "some.key"</li>
* <li>${${some.key}:${another.key}} => "some.key", "another.key"</li>
* <li>#{new java.text.SimpleDateFormat('${some.key}').parse('${another.key}')} => "some.key", "another.key"</li>
* </ul>
*/
public Set<String> extractPlaceholderKeys(String propertyString) {
Set<String> placeholderKeys = Sets.newHashSet();
if (!isNormalizedPlaceholder(propertyString) && !isExpressionWithPlaceholder(propertyString)) {
return placeholderKeys;
}
Stack<String> stack = new Stack<>();
stack.push(propertyString);
while (!stack.isEmpty()) {
String strVal = stack.pop();
int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX);
if (startIndex == -1) {
placeholderKeys.add(strVal);
continue;
}
int endIndex = findPlaceholderEndIndex(strVal, startIndex);
if (endIndex == -1) {
// invalid placeholder?
continue;
}
String placeholderCandidate = strVal.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex);
// ${some.key:other.key}
if (placeholderCandidate.startsWith(PLACEHOLDER_PREFIX)) {
stack.push(placeholderCandidate);
} else {
// some.key:${some.other.key:100}
int separatorIndex = placeholderCandidate.indexOf(VALUE_SEPARATOR);
if (separatorIndex == -1) {
stack.push(placeholderCandidate);
} else {
stack.push(placeholderCandidate.substring(0, separatorIndex));
String defaultValuePart =
normalizeToPlaceholder(placeholderCandidate.substring(separatorIndex + VALUE_SEPARATOR.length()));
if (!Strings.isNullOrEmpty(defaultValuePart)) {
stack.push(defaultValuePart);
}
}
}
// has remaining part, e.g. ${a}.${b}
if (endIndex + PLACEHOLDER_SUFFIX.length() < strVal.length() - 1) {
String remainingPart = normalizeToPlaceholder(strVal.substring(endIndex + PLACEHOLDER_SUFFIX.length()));
if (!Strings.isNullOrEmpty(remainingPart)) {
stack.push(remainingPart);
}
}
}
return placeholderKeys;
}
private boolean isNormalizedPlaceholder(String propertyString) {
return propertyString.startsWith(PLACEHOLDER_PREFIX) && propertyString.endsWith(PLACEHOLDER_SUFFIX);
}
private boolean isExpressionWithPlaceholder(String propertyString) {
return propertyString.startsWith(EXPRESSION_PREFIX) && propertyString.endsWith(EXPRESSION_SUFFIX)
&& propertyString.contains(PLACEHOLDER_PREFIX);
}
private String normalizeToPlaceholder(String strVal) {
int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX);
if (startIndex == -1) {
return null;
}
int endIndex = strVal.lastIndexOf(PLACEHOLDER_SUFFIX);
if (endIndex == -1) {
return null;
}
return strVal.substring(startIndex, endIndex + PLACEHOLDER_SUFFIX.length());
}
private int findPlaceholderEndIndex(CharSequence buf, int startIndex) {
int index = startIndex + PLACEHOLDER_PREFIX.length();
int withinNestedPlaceholder = 0;
while (index < buf.length()) {
if (StringUtils.substringMatch(buf, index, PLACEHOLDER_SUFFIX)) {
if (withinNestedPlaceholder > 0) {
withinNestedPlaceholder--;
index = index + PLACEHOLDER_SUFFIX.length();
} else {
return index;
}
} else if (StringUtils.substringMatch(buf, index, SIMPLE_PLACEHOLDER_PREFIX)) {
withinNestedPlaceholder++;
index = index + SIMPLE_PLACEHOLDER_PREFIX.length();
} else {
index++;
}
}
return -1;
}
}
3. 服务集群通过redis来实现同步修改
Redis依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.0.7.RELEASE</version>
</dependency>
RedisConfig配置
import com.zhuiyi.yicall.redis.serializer.FastjsonSerializer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
@Slf4j
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(LettuceConnectionFactory connectionFactory){
RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
/**
* 发布跟订阅
* @param connectionFactory
* @param listenerAdapter
* @return
*/
@Bean
RedisMessageListenerContainer container(LettuceConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 可以添加多个 messageListener,配置不同的交换机
container.addMessageListener(listenerAdapter, new PatternTopic("sync"));
return container;
}
}
RedisUtil工具类(发布信息)
@Slf4j
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
//发送信息
public void pushMsg(String msg){
//1. channel 2.信息
redisTemplate.convertAndSend("sync",msg);
}
}
RedisReceiver(消费信息)
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zhuiyi.yicall.common.SpringValue;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
/**
* 消费消息
*/
@Slf4j
@Component
public class RedisReceiver {
public void receiveMessage(String message) {
JSONObject object = JSON.parseObject(message);
log.info(" ====== 【 监听到的消息:{} 】 ===== ",object);
if( ! StringUtils.isEmpty(object.getString("syncKey"))){
String key = object.getString("syncKey");
String keyValue = (String) object.getOrDefault(object.getString("syncValue"), "");
Collection<SpringValue> values = SpringValueProcessor.springValueCacheMap.get(key);
for (SpringValue value : values) {
try {
log.info("消费订阅信息receiveMessage ==> beanSimpleName{}.key:【{}】old值:【{}】new值:【{}】",value.getBeanSimpleName(), key, value.getFieldValue(), keyValue);
value.update(keyValue);
} catch (IllegalAccessException e) {
log.info("消费订阅信息receiveMessage ==> key:{} , value:{},修改报错:IllegalAccessException", key, keyValue);
} catch (InvocationTargetException e) {
log.info("消费订阅信息receiveMessage ==> key:{} , value:{} ,修改报错:InvocationTargetException", key, keyValue);
}
}
}
}
/**
* 消息适配器
* 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
* @param receiver
* @return
*/
@Bean
MessageListenerAdapter listenerAdapter(RedisReceiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
}
4. Demo案例
【Controller层】SyncValueConfigController
import com.zhuiyi.yicall.callout.request.SyncValueQueryRest;
import com.zhuiyi.yicall.callout.request.SyncValueRest;
import com.zhuiyi.yicall.callout.snncvalue.SyncValueConfigService;
import com.zhuiyi.yicall.callout.vo.HttpBody;
import com.zhuiyi.yicall.po.SyncValueConfigPO;
import com.zhuiyi.yicall.po.SyncValueSystemPO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@Api(tags = "同步服务value配置")
@RequestMapping("/api")
@RestController
public class SyncValueConfigController {
@Autowired
SyncValueConfigService syncValueConfigService;
@ApiOperation(value = "修改配置项-mysql")
@PostMapping("/update")
public HttpBody<String> update(@RequestBody SyncValueRest rest) {
return HttpBody.getSucInstance(syncValueConfigService.update(rest));
}
@ApiOperation(value = "查询数据库里面的全部配置项-mysql")
@PostMapping(value = "/queryAll")
public HttpBody<List<SyncValueConfigPO>> queryAll(@RequestBody SyncValueQueryRest rest) {
return HttpBody.getSucInstance(syncValueConfigService.queryAll(rest));
}
@ApiOperation(value = "获取系统里面的(包含数据库)变量-java")
@PostMapping(value = "/querySystemValue")
public HttpBody<List<SyncValueSystemPO>> querySystemValue() {
return HttpBody.getSucInstance(syncValueConfigService.querySystemValue());
}
@ApiOperation(value = "新增配置项-mysql")
@PostMapping("/add")
public HttpBody<String> add(@RequestBody SyncValueRest rest) {
return HttpBody.getSucInstance(syncValueConfigService.add(rest));
}
@ApiOperation(value = "更新配置到bean对象里面-mysql")
@GetMapping("/renewal/{syncId}")
public HttpBody<String> renewal(@PathVariable(value = "syncId") Integer syncId) {
return HttpBody.getSucInstance(syncValueConfigService.renewal(syncId));
}
}
【Service层】SyncValueConfigService
import com.zhuiyi.yicall.callout.request.SyncValueQueryRest;
import com.zhuiyi.yicall.callout.request.SyncValueRest;
import com.zhuiyi.yicall.po.SyncValueConfigPO;
import com.zhuiyi.yicall.po.SyncValueSystemPO;
import java.util.List;
public interface SyncValueConfigService {
//修改配置
String update(SyncValueRest rest);
//查询全部
List<SyncValueConfigPO> queryAll(SyncValueQueryRest rest);
//查询系统所有变量
List<SyncValueSystemPO> querySystemValue();
//新增配置
String add(SyncValueRest rest);
//发布更新配置(更新到运行时)
String renewal(Integer syncId);
}
【Service-Impl层】SyncValueConfigServiceImpl
import com.alibaba.fastjson.JSON;
import com.zhuiyi.yicall.callout.request.SyncValueQueryRest;
import com.zhuiyi.yicall.callout.request.SyncValueRest;
import com.zhuiyi.yicall.callout.snncvalue.SyncValueConfigService;
import com.zhuiyi.yicall.common.SpringValue;
import com.zhuiyi.yicall.config.SpringValueProcessor;
import com.zhuiyi.yicall.dao.SyncValueConfigDao;
import com.zhuiyi.yicall.po.SyncValueConfigPO;
import com.zhuiyi.yicall.po.SyncValueSystemPO;
import com.zhuiyi.yicall.redis.cache.RedisUtil;
import com.zhuiyi.yicall.utils.exception.YicallError;
import com.zhuiyi.yicall.utils.exception.YicallException;
import com.zhuiyi.yicall.utils.util.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Service
public class SyncValueConfigServiceImpl implements SyncValueConfigService {
//查询数据库里面的配置数据
@Autowired
private SyncValueConfigDao syncValueConfigDao;
//redis 发布和订阅消息
@Autowired
private RedisUtil redisUtil;
/**
* 查询系统变量
* @return
*/
@Override
public List<SyncValueSystemPO> querySystemValue() {
List<SyncValueSystemPO> result = new ArrayList<>();
List<SyncValueConfigPO> pos = queryAll(new SyncValueQueryRest());
Map<String, SyncValueConfigPO> map = new HashMap<>();
if( ! pos.isEmpty()){
pos.forEach(q-> map.put(q.getSyncKey(),q));
}
//遍历系统变量 -- 数据来源是 系统启动时 通过实现BeanPostProcessor 来获取到配置项
for (Map.Entry<String, SpringValue> entry : SpringValueProcessor.springValueCacheMap.entries()) {
SyncValueSystemPO po = new SyncValueSystemPO();
String ip = "127.0.0.1";
if( ! HttpUtil.isWindowsOS()){
ip = HttpUtil.getLinuxLocalIp();
}
po.setIp(ip);
po.setSysKey(entry.getKey());
po.setSysClass(entry.getValue().getBeanName());
po.setSysBeanSimpleName(entry.getValue().getBeanSimpleName());
po.setSysOldValue(String.valueOf(entry.getValue().getOldValue()));
try {
po.setSysNewValue(String.valueOf(entry.getValue().getFieldValue()));
} catch (IllegalAccessException e) {
log.info("系统变量___获取key:【{}】的值失败:IllegalAccessException: 【{}】",entry.getKey(),e.getMessage());
}
if(map.containsKey(entry.getKey())){
SyncValueConfigPO configPO = map.get(entry.getKey());
po.setSyncId(configPO.getSyncId());
po.setSyncValue(configPO.getSyncValue());
po.setSyncComment(configPO.getSyncComment());
}
result.add(po);
}
return result;
}
//查询全部--mysql
@Override
public List<SyncValueConfigPO> queryAll(SyncValueQueryRest rest) {
return syncValueConfigDao.selectAll(rest);
}
@Override
public String update(SyncValueRest rest) {
if(rest.getSyncId() == null){
throw new YicallException(YicallError.COMMON_ERROR,"syncId不能为空");
}
strIsEmpty(rest.getSyncKey(),"key不能为空");
strIsEmpty(rest.getSyncValue(),"value不能为空");
strIsEmpty(rest.getSyncClass(),"class不能为空");
SyncValueQueryRest queryRest = new SyncValueQueryRest();
queryRest.setSyncKey(rest.getSyncKey());
queryRest.setUpdateSyncId(rest.getSyncId());
List<SyncValueConfigPO> pos = queryAll(queryRest);
if( ! pos.isEmpty()){
throw new YicallException(YicallError.COMMON_ERROR,"当前key已经存在,请不要重复创建");
}
int result = syncValueConfigDao.update(rest);
if(result != 1){
return String.format("修改失败:%s",rest.getSyncKey());
}
return String.format("修改成功:%s",rest.getSyncKey());
}
public static void strIsEmpty(String field,String msg){
if(StringUtils.isEmpty(field)){
throw new YicallException(YicallError.COMMON_ERROR,msg);
}
}
@Override
public String add(SyncValueRest rest) {
strIsEmpty(rest.getSyncKey(),"key不能为空");
strIsEmpty(rest.getSyncValue(),"value不能为空");
strIsEmpty(rest.getSyncClass(),"class不能为空");
SyncValueQueryRest queryRest = new SyncValueQueryRest();
queryRest.setSyncKey(rest.getSyncKey());
List<SyncValueConfigPO> pos = queryAll(queryRest);
if( ! pos.isEmpty()){
throw new YicallException(YicallError.COMMON_ERROR,"当前key已经存在,请不要重复创建");
}
rest.setCreateTime(new Date());
int result = syncValueConfigDao.add(rest);
if(result != 1){
return String.format("新增失败:%s",rest.getSyncKey());
}
return String.format("新增成功:%s",rest.getSyncKey());
}
@Override
public String renewal(Integer syncId) {
SyncValueQueryRest rest = new SyncValueQueryRest();
rest.setSyncId(syncId);
List<SyncValueConfigPO> pos = syncValueConfigDao.selectAll(rest);
if(pos.isEmpty()){
throw new YicallException(YicallError.COMMON_ERROR,syncId+":当前对象不存在,发布失败");
}
SyncValueConfigPO po = pos.get(0);
//发布消息
redisUtil.pushMsg(JSON.toJSONString(po));
return String.format("发布成功:%s",po.getSyncKey());
}
}
【dao层】SyncValueConfigDao
import com.zhuiyi.yicall.callout.request.SyncValueQueryRest;
import com.zhuiyi.yicall.callout.request.SyncValueRest;
import com.zhuiyi.yicall.po.SyncValueConfigPO;
import java.util.List;
public interface SyncValueConfigDao {
//查询所有
List<SyncValueConfigPO> selectAll(SyncValueQueryRest rest);
//修改
int update(SyncValueRest rest);
//新增
int add(SyncValueRest rest);
}
【mapper层】SyncValueConfigMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.zhuiyi.yicall.dao.SyncValueConfigDao" >
<insert id="add" parameterType="com.zhuiyi.yicall.callout.request.SyncValueRest">
INSERT INTO
sync_value_config
<trim prefix="(" suffix=")" suffixOverrides="," >
<if test="syncId != null" >
sync_id,
</if>
<if test="syncKey != null" >
sync_key,
</if>
<if test="syncValue != null" >
sync_value,
</if>
<if test="syncComment != null" >
sync_comment,
</if>
<if test="syncClass != null" >
sync_class,
</if>
<if test="createTime != null" >
create_time,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides="," >
<if test="syncId != null" >
#{syncId,jdbcType=INTEGER},
</if>
<if test="syncKey != null" >
#{syncKey,jdbcType=VARCHAR},
</if>
<if test="syncValue != null" >
#{syncValue,jdbcType=VARCHAR},
</if>
<if test="syncComment != null" >
#{syncComment,jdbcType=VARCHAR},
</if>
<if test="syncClass != null" >
#{syncClass,jdbcType=VARCHAR},
</if>
<if test="createTime != null" >
#{createTime},
</if>
</trim>
</insert>
<update id="update" parameterType="com.zhuiyi.yicall.callout.request.SyncValueRest">
update
sync_value_config
<set>
<if test="syncKey != null">
sync_key = #{syncKey},
</if>
<if test="syncValue != null">
sync_value = #{syncValue},
</if>
<if test="syncComment != null">
sync_comment = #{syncComment},
</if>
<if test="syncClass != null">
sync_class = #{syncClass},
</if>
update_time = now()
</set>
<where>
sync_id = #{syncId}
</where>
</update>
<select id="selectAll" resultType="com.zhuiyi.yicall.po.SyncValueConfigPO">
SELECT
sync_id AS syncId,
sync_key AS syncKey,
sync_value AS syncValue,
sync_comment AS syncComment,
sync_class AS syncClass,
create_time AS createTime,
update_time AS updateTime
FROM
sync_value_config
WHERE
1 = 1
<if test="syncKey != null and syncKey !='' ">
AND sync_key = #{syncKey}
</if>
<if test="syncClass != null and syncClass !='' ">
AND sync_class = #{syncClass}
</if>
<if test="syncId != null ">
AND sync_id = #{syncId}
</if>
<if test="updateSyncId != null ">
AND sync_id != #{updateSyncId}
</if>
</select>
</mapper>
【数据库建表】sync_value_config
CREATE TABLE `sync_value_config` (
`sync_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
`sync_key` varchar(128) NOT NULL COMMENT '配置项key',
`sync_value` text NOT NULL COMMENT '配置项value',
`sync_comment` varchar(128) NOT NULL COMMENT '注释',
`sync_class` varchar(128) NOT NULL COMMENT '配置实体类',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`sync_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='配置项目'
【pojo数据库实体类】SyncValueConfigPO
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
@ApiModel(value ="配置项实体类" )
public class SyncValueConfigPO implements Serializable {
@ApiModelProperty(value = "自增id")
private Integer syncId;
@ApiModelProperty(value = "配置项key")
private String syncKey;
@ApiModelProperty(value = "配置项value")
private String syncValue;
@ApiModelProperty(value = "注释")
private String syncComment;
@ApiModelProperty(value = "配置实体类")
private String syncClass;
@ApiModelProperty(value = "创建时间")
private String createTime;
@ApiModelProperty(value = "修改时间")
private String updateTime;
}
【controller层请求实体类】
SyncValueRest
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@Data
@ApiModel(value ="配置项修改实体类" )
public class SyncValueRest {
@ApiModelProperty(value = "自增id")
private Integer syncId;
@ApiModelProperty(value = "配置项key")
private String syncKey;
@ApiModelProperty(value = "配置项value")
private String syncValue;
@ApiModelProperty(value = "注释")
private String syncComment;
@ApiModelProperty(value = "配置实体类")
private String syncClass;
@ApiModelProperty(value = "创建时间")
private Date createTime;
}
SyncValueQueryRest
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel(value = "同步服务value查询配置")
public class SyncValueQueryRest {
@ApiModelProperty(value = "自增id")
private Integer syncId;
@ApiModelProperty(value = "配置项key")
private String syncKey;
@ApiModelProperty(value = "配置实体类")
private String syncClass;
@ApiModelProperty(value = "自增id---修改去重有用")
private Integer updateSyncId;
}
5. 前端HTML代码
<template>
<div class="search-config">
<el-table
stripe
border
v-loading="tableLoading"
:data="configData"
style="width: 100%">
<el-table-column
width="100"
prop="ip"
label="ip">
</el-table-column>
<el-table-column
prop="sysClass"
label="Class"
:filters="classFilters"
:filter-method="classFilterHandler">
</el-table-column>
<el-table-column
prop="sysKey"
label="Key">
</el-table-column>
<el-table-column
prop="sysOldValue"
label="系统默认value">
</el-table-column>
<el-table-column
prop="sysNewValue"
label="系统运行value">
</el-table-column>
<el-table-column
prop="syncId"
label="id">
</el-table-column>
<el-table-column
prop="syncValue"
label="数据库value">
</el-table-column>
<el-table-column
prop="syncComment"
label="备注">
</el-table-column>
<el-table-column
fixed="right"
label="操作"
width="200">
<template slot-scope="scope">
<el-button size="small" v-if="scope.row.syncId" @click="editConfig(scope.row)">修改</el-button>
<el-button size="small" v-else @click="addConfig(scope.row)">新增</el-button>
<el-button size="small" class="public-btn" v-if="scope.row.syncId" :type="scope.row.sysNewValue === scope.row.syncValue ? 'primary' : 'success'" @click="publicConfig(scope.row)" :loading="publicLoading">发布</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog
:title="dialogTitle"
:visible.sync="formDialogVisible"
width="40%"
:close-on-click-modal="false">
<el-form v-loading="formLoading" ref="form" :model="form" label-width="80px">
<el-form-item label="Key">
<el-input disabled v-model="form.syncKey"></el-input>
</el-form-item>
<el-form-item label="Class">
<el-input disabled v-model="form.syncClass"></el-input>
</el-form-item>
<el-form-item label="Value">
<el-input v-model="form.syncValue"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.syncComment"></el-input>
</el-form-item>
<el-form-item label="创建时间">
<el-input disabled v-model="form.createTime"></el-input>
</el-form-item>
<el-form-item label="修改时间">
<el-input disabled v-model="form.updateTime"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">提交</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script>
import Vue from 'vue'
export default {
data () {
return {
configData: [],
dialogTitle: '',
formDialogVisible: false,
form: {
syncValue: '',
syncClass: '',
syncComment: '',
syncId: '',
syncKey: '',
createTime: '',
updateTime: ''
},
classFilters: [],
submitType: '',
submitLoading: false,
tableLoading: false,
publicLoading: false,
formLoading: false
}
},
methods: {
getIp () {
return Vue.yGet(`/yicall/callout/business/setting/queryOpenConfig`).then(({data}) => {
this.ip = data.callBackUrl
return data.callBackUrl
})
},
classFilterHandler (value, row, column) {
return row.sysClass === value
},
getAllConfig () {
this.tableLoading = true
return Vue.yPost(`${this.ip}/yicall/api/querySystemValue`).then(({ data }) => {
this.configData = data
const res = {}
this.classFilters = data.reduce((item, v) => {
if (!res[v.sysClass]) {
res[v.sysClass] = true
item.push({text: v.sysClass, value: v.sysClass})
}
return item
}, [])
this.tableLoading = false
}).catch(() => {
this.tableLoading = false
})
},
submit () {
if (this.submitType === 'add') {
this.addConfigSubmit()
} else if (this.submitType === 'edit') {
this.editConfigSubmit()
}
},
addConfigSubmit () {
this.submitLoading = true
return Vue.yPost(`${this.ip}/yicall/api/add`, this.form).then(({ data }) => {
this.formDialogVisible = false
this.$message.success(data)
this.getAllConfig()
this.submitLoading = false
}).catch(() => {
this.submitLoading = false
})
},
editConfigSubmit () {
this.$delete(this.form, 'createTime')
this.$delete(this.form, 'updateTime')
this.submitLoading = true
return Vue.yPost(`${this.ip}/yicall/api/update`, this.form).then(({ data }) => {
this.formDialogVisible = false
this.$message.success(data)
this.getAllConfig()
this.submitLoading = false
}).catch(() => {
this.submitLoading = false
})
},
getCurConfig (syncId) {
this.formLoading = true
return Vue.yPost(`${this.ip}/yicall/api/queryAll`, {
syncId
}).then(({ data }) => {
let curData = data[0]
// this.form.syncClass = curData.syncClass
this.form.syncKey = curData.syncKey
this.form.syncId = syncId
this.form.syncValue = curData.syncValue
this.form.syncComment = curData.syncComment
this.form.createTime = curData.createTime
this.form.updateTime = curData.updateTime
this.formLoading = false
}).catch(() => {
this.formLoading = false
})
},
editConfig (conf) {
this.submitType = 'edit'
this.dialogTitle = '修改配置'
this.formDialogVisible = true
this.form.syncClass = conf.sysClass
this.getCurConfig(conf.syncId)
},
addConfig (conf) {
this.submitType = 'add'
this.dialogTitle = '新增配置'
this.formDialogVisible = true
this.form.syncClass = conf.sysClass
this.form.syncKey = conf.sysKey
this.form.syncValue = ''
this.form.syncComment = ''
this.form.createTime = ''
this.form.updateTime = ''
},
publicConfig (conf) {
this.publicLoading = true
return Vue.yGet(`${this.ip}/yicall/api/renewal/${conf.syncId}`).then(({ data }) => {
this.$message.success(data)
this.getAllConfig()
this.publicLoading = false
}).catch(() => {
this.publicLoading = false
})
}
},
mounted () {
this.getIp().then(ip => {
if (ip) {
this.getAllConfig()
} else {
this.$message({
duration: 0,
showClose: true,
message: '当前业务未配置同步服务地址',
type: 'error'
})
}
})
}
}
</script>
<style lang="scss" scoped>
.search-config {
padding: 20px;
.public-btn {
&.el-button--primary {
background-color: #adadad;
border-color: #adadad;
}
}
}
</style>
6. 测试案例 (图片)
把当前的促激活主叫号码:改成888888
日志打印输出:
最终加载到运行时类中: