美文网首页
SpringBoot 之Servlet启动前和配置热更新

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

作者: 小马将过河 | 来源:发表于2019-06-09 23:53 被阅读0次

    有个项目里甲方粑粑提了个需求:给所有手机号默认没天最多发送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()方法知性之后执行

    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 {
        @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的解决办法

    相关文章

      网友评论

          本文标题:SpringBoot 之Servlet启动前和配置热更新

          本文链接:https://www.haomeiwen.com/subject/lwpoxctx.html