有个项目里甲方粑粑提了个需求:给所有手机号默认没天最多发送50条短信,但是可以改,修改成51的话,那就每天再多发一条呗。
需求听起来很简单,把50存在数据库的system_config表中,每次发送时,查询然后判断下。
配置在配置文件中热更新可以使配置生效,但是是不能满足需求,因为修改后的值没有保存容器重启后就失效。
但是聪明如你,这一定不是最优的。
这个需求不是我实现的,但是我参与了方案讨论,现在已经实现,在此记录下方案和方式,以备不时之需。
方案一:
1、默认配置存储在DB中
2、容器启动后从DB中读取配置数据,并存入Redis,后面使用时以redis为准,如果redis丢失了,从DB重新获取。
3、修改配置时修改redis中的配置数据,并同步修改DB里的数据。
引入redis的好处时每次发送时无需进行DB级的查询,提高相应效率。
方案二:
1、默认配置存储在DB中
2、容器启动后,将从DB中查询到的数据,创建出一个配置文件。
3、修改配置时修改DB中的数据,同时使用配置热更新,修改配置文件中的值。
很明显重点是容器启动后读取DB中的数据
,本文使用方案二,上源码:
系统属性table
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
import lombok.Data;
import lombok.ToString;
/**
* 配置文件数据表
*
*/
@ToString
@Data
@Entity
@Table(name = "t_sys_config")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class SysConfig {
/**
* 主键
*/
@Id
@GenericGenerator(name = "snowFlakeId", strategy = "com.hczt.haier.commoncenter.jpa.SnowflakeIdentifierGenerator")
@GeneratedValue(generator = "snowFlakeId")
private Long id;
/**
* 属性编码
*/
private String code;
/**
* 属性值
*/
private String value;
/**
* 属性名称
*/
private String name;
/**
* 属性描述
*/
private String description;
}
加载SystemConfig
import java.util.Map;
import java.util.Properties;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import com.hczt.haier.smscenter.db.entity.SysConfig;
import com.hczt.haier.smscenter.service.SysConfigService;
import lombok.extern.slf4j.Slf4j;
/**
* 系统属性配置
*
*/
@Slf4j
@Configuration
public class SystemConfig {
@Autowired
private ConfigurableEnvironment environment;
@Autowired
private SysConfigService sysConfigService;
/**
* 从数据库加载配置文件信息
*/
@PostConstruct
public void initDbPropertySourceUsage() {
log.info("从数据库中加载属性配置文件开始");
// 获取系统属性集合
MutablePropertySources propertySources = environment.getPropertySources();
// 从数据库中获取自定义变量表
Map<String, String> collect = sysConfigService.findAll().stream().collect(Collectors.toMap(SysConfig::getCode, SysConfig::getValue));
log.info("从数据库中获取自定义变量表完成,获取到的数据为【{}】", collect);
// 将转换后的列表加入属性中
Properties properties = new Properties();
properties.putAll(collect);
PropertiesPropertySource constants = new PropertiesPropertySource("system-config", properties);
// 定义寻找属性的正则,该正则为系统默认属性集合的前缀
Pattern pattern = Pattern.compile("^application*");
// 接收系统默认属性集合的名称
String name = null;
// 接收是否找到系统默认属性集合
boolean flag = false;
// 遍历属性集合
for (PropertySource<?> source : propertySources) {
// 正则匹配
if (pattern.matcher(source.getName()).matches()) {
// 接收名称
name = source.getName();
// 变更标识
flag = true;
break;
}
}
if (flag) {
// 找到则将自定义属性添加到该属性前
propertySources.addBefore(name, constants);
} else {
// 未找到则默认添加到第一位
propertySources.addFirst(constants);
}
log.info("从数据库中加载属性配置文件结束");
}
}
里面的SysConfigService及不打了,直接调用了Jpa的Repository而已。
@PostConstruct
注解正是为解决容器启动后干啥
而生,与其相配的是@PreDestroy
容器销毁前干点啥。
被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。PreDestroy()方法在destroy()方法知性之后执行
另外,spring中Constructor、@Autowired、@PostConstruct的顺序其实从依赖注入的字面意思就可以知道,要将对象p注入到对象a,那么首先就必须得生成对象a和对象p,才能执行注入。
所以,如果一个类A中有个成员变量p被@Autowried注解,那么@Autowired注入是发生在A的构造方法执行完之后的。如果想在生成对象时完成某些初始化操作,而偏偏这些初始化操作又依赖于依赖注入,那么久无法在构造函数中实现。为此,可以使用@PostConstruct注解一个方法来完成初始化,@PostConstruct注解的方法将会在依赖注入完成后被自动调用。
定义属性刷新监听器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义属性刷新监听器
* 当调用/actuator/refresh接口时会调用onRefresh方法
*
*/
@Slf4j
@Configuration
public class PropertiesRefreshEventListener {
@Autowired
private SystemConfig systemConfig;
/**
* 属性刷新事件监听方法
* 当此方法被调用时,表示/actuator/refresh方法被调用
* @param event
*/
@EventListener(RefreshScopeRefreshedEvent.class)
public void onRefresh(RefreshScopeRefreshedEvent event) {
log.info("重新从数据库装载属性");
systemConfig.initDbPropertySourceUsage();
}
}
启动配置热更新
/**
* 短信发送接口(采用redis channel 实现 pub/sub 模式)
* @param ipInfo 客户端ip信息
* @param appInfo 请求应用信息
* @param msgInfo 短信发送信息
* @return
*/
@RefreshScope
@PostMapping(value="/send")
public RtnResult<String> sendSmsCodeChannel(IpInfo ipInfo,
AppInfo appInfo,
@Validated @RequestBody MsgInfo msgInfo,
@Value("${smsCode.limitCount}") Long smsLimitCount) {
log.info("应用【{}】IP地址【{}】采用Redis Channel方式请求短信发送接口,请求参数为【{}】",
appInfo.getAppId(),ipInfo.getIpAddress(), msgInfo);
// 判断短信发送次数是否超限(每应用每天每手机号)
if(checkSmsCountOver(appInfo, msgInfo,smsLimitCount)) {
return RtnResult.error(SmsCodeMsg.SMS_COUNT_OVER.fillArgs(msgInfo.getMobile()));
}
long start = System.currentTimeMillis();
// 组装发送队列条件
AsyncSmsInfo asyncSmsInfo = new AsyncSmsInfo();
asyncSmsInfo.setAppId(appInfo.getAppId());
asyncSmsInfo.setAppName(appInfo.getAppName());
asyncSmsInfo.setMobileSign(appInfo.getMobileSign());
asyncSmsInfo.setSmsCodeExpire(appInfo.getSmsCodeExpire());
asyncSmsInfo.setSmsCodeLength(appInfo.getSmsCodeLength());
asyncSmsInfo.setMobile(msgInfo.getMobile());
asyncSmsInfo.setContent(msgInfo.getContent());
asyncSmsInfo.setIpAddress(ipInfo.getIpAddress());
asyncSmsInfo.setInvokeTime(new Date());
try {
// 异步发送短信,将信息推送到Redis消息服务队列
publisherService.pushSms(asyncSmsInfo);
} catch (BizException e) {
return RtnResult.error(SmsCodeMsg.SMS_PUBLISH_ERROR);
}
long end = System.currentTimeMillis();
log.info("应用【{}】IP地址【{}】采用Redis Channel方式请求短信发送接口,请求参数为【{}】完成,执行时间为【{}】ms", appInfo.getAppId(),ipInfo.getIpAddress(), msgInfo, end-start);
return RtnResult.success(StringUtils.EMPTY);
}
文中提到的redis channel发送消息模式不是重点,不在讲解使用的是redisTemplate的convertAndSend方法,有需要时自行查阅。
参考
@PostConstruct
SpringCloud配置热更新@RefreshScope,以及没有出现/refresh的动态刷新地址,访问404的解决办法
网友评论