SpringBoot 之Servlet启动前和配置热更新


需要在servlet启动后和销毁前做点啥?需要热更新配置文件?本文记录这两个操作

简书地址

有个项目里甲方粑粑提了个需求:给所有手机号默认没天最多发送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("从数据库中获取自定义变量表完成,获取到的数据为【&#123;&#125;】", 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) &#123;
            
                // 正则匹配
                if (pattern.matcher(source.getName()).matches()) &#123;
                    // 接收名称
                    name = source.getName();
                    // 变更标识
                    flag = true;
                    
                    break;
                &#125;
        &#125;
        
        if (flag) &#123;
                // 找到则将自定义属性添加到该属性前
                propertySources.addBefore(name, constants);
        &#125; else &#123;
                // 未找到则默认添加到第一位
                propertySources.addFirst(constants);
        &#125;
        
        log.info("从数据库中加载属性配置文件结束");
    &#125;
&#125;

里面的SysConfigService及不打了,直接调用了Jpa的Repository而已。

@PostConstruct注解正是为解决容器启动后干啥而生,与其相配的是@PreDestroy容器销毁前干点啥。
被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。PostConstruct在构造函数之后执行,init()方法之前执行。PreDestroy()方法在destroy()方法知性之后执行
postconstruct
另外,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 &#123;
    @Autowired
    private SystemConfig systemConfig;
    
    /**
     * 属性刷新事件监听方法
     * 当此方法被调用时,表示/actuator/refresh方法被调用
     * @param event
     */
    @EventListener(RefreshScopeRefreshedEvent.class)
    public void onRefresh(RefreshScopeRefreshedEvent event) &#123;
        log.info("重新从数据库装载属性");
        systemConfig.initDbPropertySourceUsage();
    &#125;
&#125;

启动配置热更新

    /**
     * 短信发送接口(采用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("$&#123;smsCode.limitCount&#125;") Long smsLimitCount) &#123;
        log.info("应用【&#123;&#125;】IP地址【&#123;&#125;】采用Redis Channel方式请求短信发送接口,请求参数为【&#123;&#125;】",
                appInfo.getAppId(),ipInfo.getIpAddress(), msgInfo);
        
        // 判断短信发送次数是否超限(每应用每天每手机号)
        if(checkSmsCountOver(appInfo, msgInfo,smsLimitCount)) &#123;
            return RtnResult.error(SmsCodeMsg.SMS_COUNT_OVER.fillArgs(msgInfo.getMobile()));
        &#125;

        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 &#123;
            // 异步发送短信,将信息推送到Redis消息服务队列
            publisherService.pushSms(asyncSmsInfo);
        &#125; catch (BizException e) &#123;
            return RtnResult.error(SmsCodeMsg.SMS_PUBLISH_ERROR);
        &#125;
        
        long end = System.currentTimeMillis();
        log.info("应用【&#123;&#125;】IP地址【&#123;&#125;】采用Redis Channel方式请求短信发送接口,请求参数为【&#123;&#125;】完成,执行时间为【&#123;&#125;】ms", appInfo.getAppId(),ipInfo.getIpAddress(), msgInfo, end-start);
        return RtnResult.success(StringUtils.EMPTY);
    &#125;

文中提到的redis channel发送消息模式不是重点,不在讲解使用的是redisTemplate的convertAndSend方法,有需要时自行查阅。

参考

@PostConstruct
SpringCloud配置热更新@RefreshScope,以及没有出现/refresh的动态刷新地址,访问404的解决办法


评论
  目录