当前位置:   article > 正文

基于Spring Cloud的微服务架构脚手架实践_微服务脚手架

微服务脚手架

文章目录

1 前言

-----------------------------------------------------------------
小伙伴们运气不错呀,刷到我这个博客了,这篇博客可是融合了我不少心血,两万多字的详细教程,满满的干货,先来个三连击吧,点赞收藏加关注
-----------------------------------------------------------------

工作了很多年,都没有自己的一个项目脚手架,所以说,前阵子就准备搞一个自己的Spring Cloud微服务的架构。Spring Cloud 官网,2021-07-06 发布了Hoxton.SR12 这个版本, 本来想使用 Hoxton.SR12这个Spring Cloud版本,查了一些资料,发现基于这个版本,好用的微服务架构体系并且开源的项目不是很多,可能是这个版本刚出来两三个月,就自己折腾了一个基础架构。在进行依赖管理的过程中,走了不少坑,各种jar冲突或者版本不兼容等等,这里总结记录下,防止以后再次踩坑。

为了搭建自己的脚手架,方便以后项目的管理和维护,现在开发了这一套基础脚手架项目,该项目基于 Spring Boot+ Spring Cloud + MyBatis-Plus+Spring Cloud Alibaba,为了提高项目的开发效率,降低项目的维护成本,避免重复造轮子,我基于我上一篇博客 Spring Cloud 微服务基础功能架构来啦 中的基础架构,搭建了一套脚手架,可以直接拿来使用。

2 脚手架主要提供哪些功能

该项目主要包括以下功能模块:

  • 统一管理项目依赖,核心依赖的版本控制
  • 缓存管理以及分布式锁的处理
  • 分布式id功能
  • 预警通知功能
  • 异常管理
  • 国际化功能
  • 限流Api管理
  • Mock Server管理
  • 消息中间件MQ管理
  • 操作日志管理
  • 轻量级流程管理
  • 定时任务管理
  • 项目安全管理
  • Swagger-Ui管理
  • 工具类管理
  • 网关服务管理

3 如何使用该脚手架

3.1 项目统一依赖管理

  • 集成基础设施项目:

    在自己的maven项目中,在最顶层项目的pom文件中,继承该基础设施项目:

        <parent>
            <groupId>cn.smilehappiness</groupId>
            <artifactId>smilehappiness-architecture</artifactId>
            <version>1.0.0</version>
        </parent>
    
    • 1
    • 2
    • 3
    • 4
    • 5

3.2 集成基础模块功能到自己的项目中

下面的这些依赖的功能模块,可以根据实际情况,选择集成哪些功能模块,添加如下依赖,即可把基础设施项目的核心功能,集成到自己的项目中:

	<dependency>
	   <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-cache</artifactId>
	    <exclusions>
	        <exclusion>
	            <artifactId>HdrHistogram</artifactId>
	            <groupId>org.hdrhistogram</groupId>
	        </exclusion>
	    </exclusions>
	</dependency>
	<dependency>
	    <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-common</artifactId>
	</dependency>
	<dependency>
      <groupId>cn.smilehappiness</groupId>
        <artifactId>smilehappiness-distribute-id</artifactId>
    </dependency>
	<dependency>
	    <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-early-warning-notice</artifactId>
	</dependency>
	<dependency>
	    <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-exception</artifactId>
	</dependency>
	<dependency>
	    <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-limit-api</artifactId>
	</dependency>
	<dependency>
	    <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-mq</artifactId>
	</dependency>
	<dependency>
	    <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-operation-log</artifactId>
	</dependency>
	<dependency>
      <groupId>cn.smilehappiness</groupId>
        <artifactId>smilehappiness-process</artifactId>
    </dependency>
	<dependency>
	    <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-schedule</artifactId>
	</dependency>
	<dependency>
	    <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-swagger-ui</artifactId>
	</dependency>
	<dependency>
	    <groupId>cn.smilehappiness</groupId>
	    <artifactId>smilehappiness-utils</artifactId>
	</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54

4 基础核心功能模块的使用

4.1 集成缓存管理模块

分布式、微服务背景下,对于性能的要求也越来越高,所以缓存越来越受到了重视。现在使用比较流行的缓存是Redis,所以,笔者也基于Redis做缓存处理。

Redis常见的场景有: 普通缓存分布式锁分布式限流幂等性校验短信登录限定次数等等

4.1.1 添加cache模块依赖

     <dependency>
          <groupId>cn.smilehappiness</groupId>
          <artifactId>smilehappiness-cache</artifactId>
     </dependency>
  • 1
  • 2
  • 3
  • 4

4.1.2 cache模块的功能使用

  • Redis工具的基本使用示例

    注入以下工具类:

    	@Autowired
        private RedissonClient redissonClient;
        @Autowired
        private RedisUtil redisUtil;
        @Autowired
        private RedissonLockRedisUtil redissonLockRedisUtil;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后使用以下测试用例:

    public void testRedisUtil() {
        //赋值
        redisUtil.set("test1", "你好");
        //该工具类,默认过期单位为秒
        redisUtil.set("test2", "测试一下过期时间", 30);
    
        //取值
        System.out.println(redisUtil.get("test1"));
        System.out.println(redisUtil.get("test2"));
    
        //删除值
        redisUtil.del("test1");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
  • Redis分布式锁工具的基本使用示例

    /**
     * <p>
     * 测试分布式锁的使用,基于Redisson客户端实现(该方法的实现推荐使用)
     * <p/>
     *
     * @param
     * @return void
     * @Date 2021/10/4 16:51
     */
    @Test
    public void testDistributeLock() {
        String bizLockKey = "smilehappiness:trialOrder:orderNumberxxxxxxx";
    
        //支持过期解锁功能,10秒钟以后自动解锁, 无需调用unlock方法手动解锁,当然,为了不占用资源,使用锁处理完业务,一般还是建议手动释放锁
        RLock lock = redissonLockRedisUtil.lock(bizLockKey, 60L);
        if (lock.tryLock()) {
            try {
                //处理业务方法。。。
            } catch (Exception e) {
                log.error("获取分布式锁失败,失败原因:{}", e);
                throw new SystemInternalException("获取分布式锁失败,失败原因:" + e.getMessage());
            } finally {
                lock.unlock();
            }
        } else {
            log.error("系统繁忙,请稍后再试!");
            throw new BusinessException("系统繁忙,请稍后再试!");
        }
    }
    
    /**
     * <p>
     * 测试分布式锁的使用,基于Redisson客户端实现(该方法的实现推荐使用)
     * <p/>
     *
     * @param
     * @return void
     * @Date 2021/10/4 16:51
     */
    @Test
    public void testDistributeLockTwo() {
        String bizLockKey = "smilehappiness:trialOrder:orderNumberxxxxxxx";
    
        //尝试加锁,最多等待30秒,上锁以后120秒自动解锁
        boolean lockFlag = redissonLockRedisUtil.tryLock(bizLockKey, 30L, 2 * 60L);
        if (lockFlag) {
            try {
                //处理业务方法。。。
            } catch (Exception e) {
                log.error("获取分布式锁失败,失败原因:{}", e);
                throw new SystemInternalException("获取分布式锁失败,失败原因:" + e.getMessage());
            } finally {
                redissonLockRedisUtil.unlock(bizLockKey);
            }
        } else {
            log.error("系统繁忙,请稍后再试!");
            throw new BusinessException("系统繁忙,请稍后再试!");
        }
    }
    
    /**
     * <p>
     * 测试分布式锁的使用,基于Redisson客户端实现方式
     * <p/>
     *
     * @param
     * @return void
     * @Date 2021/10/4 16:51
     */
    @Test
    public void testDistributeLockOriginal() {
        String bizLockKey = "smilehappiness:trialOrder:orderNumberxxxxxxx";
        RLock lock = redissonClient.getLock(bizLockKey);
    
        if (lock.tryLock()) {
            try {
                //处理业务方法。。。
            } catch (Exception e) {
                log.error("获取分布式锁失败,失败原因:{}", e);
                throw new SystemInternalException("获取分布式锁失败,失败原因:" + e.getMessage());
            } finally {
                lock.unlock();
            }
        } else {
            log.error("系统繁忙,请稍后再试!");
            throw new BusinessException("系统繁忙,请稍后再试!");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

4.2 集成通知预警管理模块

目前,只设计了钉钉预警通知,后续可以集成邮件通知等等

4.2.1 添加通知预警模块依赖

     <dependency>
          <groupId>cn.smilehappiness</groupId>
          <artifactId>smilehappiness-early-warning-notice</artifactId>
     </dependency>
  • 1
  • 2
  • 3
  • 4

4.2.2 添加yml配置

在yml文件或者properties配置文件中添加如下内容:

# 钉钉预警通知
earlyWarning:
  notice:
    # 一般预警通知
    generalDingNoticeUrl: xxx
    # 高频异常通知预警
    errorDingNoticeUrl: xxx
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

记录好机器人的Webhook 地址,可以在自己项目中调用此地址向群聊发送相关消息通知,做到项目异常的预警通知或者一些其它业务通知,至此已经设置完成,剩下的只需要自己在项目中需要预警的地方调用接口通知即可。

如果你设定的机器人类型是关键字,内容需要包含关键字,才可以发送通知成功

注:这里分为了两个地址,可以根据实际情况,来决定用一个还是用两个,使用的时候非常简单,在钉钉上创建一个机器人,把webhook地址复制过来即可,限于篇幅,就不再详细说明,玩不转的小伙伴,可以参考资料:https://blog.csdn.net/nbskycity/article/details/106068455

4.2.3 钉钉预警使用示例

  • 首先注入工具类

    	 @Autowired
        private DingTalkWarningNoticeServer dingTalkWarningNoticeServer;
    
    • 1
    • 2
  • 参考代码示例

    /**
     * 钉钉预警通知测试,注意,如果你设定的机器人类型是关键字,内容需要包含关键字,才可以发送通知成功
     */
    @Test
    public void testDingTalkNotice() {
        dingTalkWarningNoticeServer.sendWarningMessage("smile:这是一个警告的通知");
        //第一个参数title,可以理解为关键信息标识,后续跟踪日志可以使用该关键信息标识快速找到
        dingTalkWarningNoticeServer.sendWarningMessage("hello,", "smile:这是一个警告的通知");
    
        try {
            log.info("结果:{}", 1 / 0);
        } catch (Exception e) {
            dingTalkWarningNoticeServer.sendErrorMessage(StringUtils.join("smile:异常通知,原因:", e.getMessage()));
            dingTalkWarningNoticeServer.sendErrorMessage("world", StringUtils.join("smile:异常通知,原因:", e.getMessage()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

4.3 集成异常管理模块

项目中,经常会遇到各种各样的异常,有时候异常信息可以给用户看,比如说:银行卡号填写错误、邮箱格式不合法等、而有的信息不能给用户看,比如说:系统走神了…

还有一种场景,针对异常信息,有时候需要进行文字翻译,比如:系统返回的失败信息是 AAA,可能需要在适配层翻译为BBB返回给用户,可以做对照表等等进行处理

4.3.1 添加异常模块依赖

    <dependency>
        <groupId>cn.smilehappiness</groupId>
        <artifactId>smilehappiness-exception</artifactId>
    </dependency>
  • 1
  • 2
  • 3
  • 4

4.3.2 使用异常

4.3.2.1 两种异常说明

目前定义了两种异常: 一种是业务异常,另外一种是系统级异常

  • 使用业务异常(BusinessException)时,异常信息会直接返回给用户端,业务异常支持添加了普通code,业务code,以及异常信息。在设计的时候,业务异常类这里额外添加一个业务bizCode参数为了后续扩展性更强(可以基于业务bizCode做不同的信息对照展示,对于前端而言,基于普通的code,200或者非200即可判断是否请求接口成功)

  • 使用系统级异常(SystemInternalException)时,针对这种系统异常,会统一降级处理,比如可以友好的返回:系统升级中,请您稍后再试...,而不是返回系统走神了或者一大串英文异常给客户

4.3.2.2 异常使用示例

具体使用可参考如下示例:

    @GetMapping("/getApiLoggerInfoByRequestUrlAndMethodName")
    public CommonResult<List<ApiLogger>> getApiLoggerInfoByRequestUrlAndMethodName(@RequestParam("requestUrl") String requestUrl, @RequestParam("methodName") String methodName) {
        LocalDateTime bizTimeStart = LocalDateTime.now();
        CommonResult<List<ApiLogger>> commonResult = new CommonResult<>();
        try {
            List<ApiLogger> apiLoggerList = apiLoggerService.getApiLoggerInfoByRequestUrlAndMethodName(requestUrl, methodName);
            if (CollectionUtils.isEmpty(apiLoggerList)) {
                throw new BusinessException(FrameworkBusinessExceptionEnum.API_LOGGER_INFO_NULL);
            }

            log.info("通过请求url和方法名称,获取日志信息接口返回结果:{}", JSON.toJSONString(commonResult));
            log.info("通过请求url和方法名称,获取日志信息方法执行耗时(毫秒):{}", DateUtil.getTakeTime(bizTimeStart, LocalDateTime.now(), TimeUnit.MILLISECONDS));

            return commonResult;
        } catch (BusinessException e) {
            log.error("【业务异常】通过请求url和方法名称,获取日志信息异常,异常原因:{}", e.getMessage());
            log.info("通过请求url和方法名称,获取日志信息方法执行耗时(毫秒):{}", DateUtil.getTakeTime(bizTimeStart, LocalDateTime.now(), TimeUnit.MILLISECONDS));

            throw new BusinessException(e.getCode(), e.getBizCode(), "通过请求url和方法名称,获取日志信息异常,异常原因:" + e.getMessage());
        } catch (Exception e) {
            log.error("【系统异常】通过请求url和方法名称,获取日志信息异常,异常原因:{}", e.getMessage());
            log.info("通过请求url和方法名称,获取日志信息方法执行耗时(毫秒):{}", DateUtil.getTakeTime(bizTimeStart, LocalDateTime.now(), TimeUnit.MILLISECONDS));

            throw new SystemInternalException("通过请求url和方法名称,获取日志信息异常,异常原因:" + e.getMessage());
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

4.4 集成限流管理模块

4.4.1 前言

为了满足各种应用场景,有时候不得不对接口Api进行限流。比如说:短信服务,供应商可能会要求每秒访问不超过400条,如果超过了这个访问量,请求就会被供应商拒绝,从而导致漏发短信。

还有的接口,第三方Api会做限制,他们为了限制访问,设定一分钟只能请求接口20次,超过了就会超时或者响应异常。

总而言之,限流,在好多场景用的还是挺多的。该模块功能,主要基于Redis提供了基础的api限流次数,具体的限流方案有很多,可以具体场景具体选择

4.4.2 添加限流管理模块依赖

     <dependency>
          <groupId>cn.smilehappiness</groupId>
          <artifactId>smilehappiness-limit-api</artifactId>
     </dependency>
  • 1
  • 2
  • 3
  • 4

4.4.3 实现的思路

思路: 借助于RedisINCR操作来实现Limit限流

  • 将INCR key中储存的数字值增一,如果key不存在,那么key的值会先被初始化为 0 ,然后再执行INCR操作。

    如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误,本操作的值限制在 64 位(bit)有符号数字表示之内。

  • 当API被调用时,在调用API前进行INCR key,key可以是ip地址相关,用户相关,业务参数相关,或是全局的一个key
    。如果返回值为1,则表示刚开始调用,赋予key过期时间,然后判断返回值是否大于设定的Limit限流数量,如果大于抛异常或者阻塞重试。

4.4.4 如何使用该模块功能进行限流

主要就是对需要限流的业务方法,添加了 @ApiLimit(limitCounts = 10, timeSecond = 120, limitApiName = "sendSmsMessage")
注解参数说明: limitCounts标识多少次开始限流,timeSecond 标识多少时间,单位秒,limitApiName标识限流的接口api业务key

以上注解含义: 表示2分钟只允许Api方法被调用10次,否则就会限流。第一次限流进行重试,如果10秒后还不能调用,则抛出异常,待业务端处理。

代码示例中,使用了两种方式进行拦截,一种是定义切入点的方式,一种是直接拦截使用ApiLimit注解的方法,这两种方式都可以。

限于篇幅,这里就不详细讲了,有兴趣的小伙伴,可以参考我之前写的另一篇博文,写的非常详细:分布式环境下,基于Redis实现Restful API接口的限流

4.4.5 代码示例

参考代码如下:

	/**
     * <p>
     * 根据消息模板以及内容,发送短信
     * <p/>
     *
     * @param smsMessage
     * @return void
     * @Date 2020/7/6 21:35
     */
    void sendSmsMessage(SmsMessage smsMessage);

	/**
     * <p>
     * 根据消息模板以及内容,发送短信
     * 默认一分钟,限流500次,可以根据实际情况进行限流
     * <p/>
     *
     * @param smsMessage
     * @return void
     * @Date 2020/7/6 21:35
     */
    @ApiLimit(limitCounts = 10, timeSecond = 120, limitApiName = "sendSmsMessage")
    @Override
    public void sendSmsMessage(SmsMessage smsMessage) {
        // 注意:一般三方可能会限制,400/s,即每秒最多发送400条,超过这个限制的短信发送请求会被拒绝,所以需要限流,在高流量下,需要在业务端限制,每秒访问不要超过400次
        // 这里只是模拟这种限流的场景,具体的限流大小,根据实际场景去设置

        // TODO 调用第三方发送短信
        System.out.println("【" + Thread.currentThread().getName() + "】 调用三方短信服务,发送短信成功!");
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

测试用例如下:

	@Resource
    private SmsMessageService smsMessageService;

    /**
     * <p>
     * 消息发送服务,限流功能测试-单条不会限流
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/7/5 22:25
     */
    @Test
    public void testSmsMessageSend() {
        SmsMessage smsMessage = new SmsMessage();
        smsMessage.setMsgKey("register-user");
        smsMessage.setContent("register an user notice!");
        smsMessageService.sendSmsMessage(smsMessage);
    }

    /**
     * <p>
     * 消息发送服务,限流功能多线程环境下测试(超过限定次数就会限流)
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/7/5 22:45
     */
    @Test
    public void testSmsMessageSendBatch() {
        //循环次数
        int count = 15;

        int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
        ThreadFactory nameThreadFactory = new ThreadFactoryBuilder().setNameFormat("smsLimit-pool-%d").build();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, corePoolSize * 2 + 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000), nameThreadFactory);
        CountDownLatch countDownLatch = new CountDownLatch(count);

        for (int i = 0; i < count; i++) {
            threadPoolExecutor.execute(() -> {
                SmsMessage smsMessage = new SmsMessage();
                smsMessage.setMsgKey("register-user");
                smsMessage.setContent("register an user notice!");
                //业务方法,添加了@ApiLimit(limitCounts = 10, timeSecond = 120)注解,表示2分钟只允许10次调用,否则就会限流
                smsMessageService.sendSmsMessage(smsMessage);
                countDownLatch.countDown();
            });
        }

        try {
            boolean await = countDownLatch.await(60, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            threadPoolExecutor.shutdown();
        }

        System.out.println("处理完成啦...");
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

4.5 集成Mock Server管理模块

先来给小伙伴普及下,Mock Server

4.5.1 MockServer是什么

MockServer其实就是一个用来模拟http(https)请求响应结果数据的服务器。通过这个MockServer服务,我们可以极大地方便接口的调试。

4.5.2 为什么要使用MockServer

项目开发中,有时候要模拟接口的各种场景的返回数据,那么,不搭建一套Mock Server服务,还能愉快地玩耍吗?如今的业务系统模块越来越多,功能也越来越复杂。及时的与前端调试也迎来了一些小的挑战。

假设有一个场景:
新项目刚开始启动时,这时候后台部分的接口都没有开发完成,这时候如果前端需要调试页面,该怎么调试呢?

傻傻的等着后台开发完成再进行调试?不可能的,这样你会影响项目正常上线。那么模拟数据就显得非常重要了,如何快速有效的模拟真实场景的数据?

有两种方案:

  • 通常情况下,后台会把请求接口Api的结果先定义好,写死在action层,然后返回给前端,但是这种方案现在已经不怎么用了,效率太低
  • 现在比较流行的方案,一般会搭建一些server来进行mock,这样可以使得被开发功能的调试和测试功能能够正常进行下去。而MockServer就可以有效的解决这个问题,这也是MockServer的出现的原因

网上找了张图片,可以很好的说明使用MockServer前后的不同,如下图所示:
使用mock之前:
在这里插入图片描述
使用mock之后:
在这里插入图片描述
使用了Mock Server之后,前端可以不再依赖与后台的业务接口,在后台接口未开发完成时,可以模拟一些业务数据,来进行前台页面的调试,极大的节省了调试的成本

4.5.3 Mock Server部署

smilehappiness-architecture项目中,Mock Server项目我已经开发好了,有一个smilehappiness-mock-server模块,该模块可以直接打jar包独立部署,把打包的mock-server-java.jar直接部署到服务器即可。

4.5.4 Mock Server的使用

Mock Server的使用一句两句讲不清楚,限于篇幅,我就不详细介绍了,我之前写了一篇很详细的教程,需要的小伙伴,可以参考我另一篇博文: 模拟数据利器之Mock Server使用教程来啦

4.6 集成MQ管理模块

在项目开发中,消息中间件服务用的也比较多,这里简单总结一下经常用到的两个消息中间件:RabbitMQRocketMQ

4.6.1 使用RabbitMQ

这里以RabbitMQ接入为例

4.6.2 基本概念

它是采用Erlang语言实现的AMQP(Advanced Message Queued Protocol)的消息中间件,最初起源于金融系统,用在分布式系统存储转发消息,目前广泛应用于各类系统用于解耦、削峰。

4.6.3 特点

  • 可靠性:通过支持消息持久化,支持事务,支持消费和传输的ack等来确保可靠性
  • 路由机制:支持主流的订阅消费模式,如广播,订阅,headers匹配等
  • 扩展性:多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。
  • 高可用性:队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队仍然可用。
  • 多种协议:RabbitMQ除了原生支持AMQP协议,还支持STOMP,MQTT等多种消息中间件协议。
  • 多语言客户端:RabbitMQ几乎支持所有常用语言,比如Jav a、Python、Ruby、PHP、C#、JavaScript等。
  • 管理界面:RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。
  • 插件机制:RabbitMQ提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。

4.6.4 概念理解

下图为rabbitmq的内部结构图

下面逐一进行解释说明:

4.6.4.1 Message

具体的消息,包含消息头(即附属的配置信息)和消息体(即消息的实体内容)

由发布者,将消息推送到Exchange,由消费者从Queue中获取

4.6.4.2 Publisher

消息生产者,负责将消息发布到交换器(Exchange)

4.6.4.3 Exchange

交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列

4.6.4.4 Binding

绑定,用于给Exchange和Queue建立关系,从而决定将这个交换器中的哪些消息,发送到对应的Queue

4.6.4.5 Queue

消息队列,用来保存消息直到发送给消费者

它是消息的容器,也是消息的终点

一个消息可投入一个或多个队列

消息一直在队列里面,等待消费者连接到这个队列将其取走

4.6.4.6 Connection

连接,内部持有一些channel,用于和queue打交道

4.6.4.7 Channel

信道(通道),MQ与外部打交道都是通过Channel来的,发布消息、订阅队列还是接收消息,这些动作都是通过Channel完成;

简单来说就是消息通过Channel塞进队列或者流出队列

4.6.4.8 Consumer

消费者,从消息队列中获取消息的主体

4.6.4.9 Virtual Host

虚拟主机,表示一批交换器、消息队列和相关对象。

虚拟主机是共享相同的身份认证和加密环境的独立服务器域。

每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。

vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 /

可以理解为db中的数据库的概念,用于逻辑拆分

4.6.4.10 Broker

消息队列服务器实体

4.6.5 添加MQ模块依赖

    <dependency>
        <groupId>cn.smilehappiness</groupId>
        <artifactId>smilehappiness-mq</artifactId>
    </dependency>
  • 1
  • 2
  • 3
  • 4

4.6.6 配置MQ相关配置

smilespring:
  rabbit:
    username: smile
    password: 123456
    port: 15672
    host: xxx
    virtualHost: /
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

4.6.7 使用demo

这里网上一大堆,省略

4.6.8 RabbitConfig 配置类解析

该类位 mq moudle下,启动时将根据配置文件生成若干 rabbitTemplate 和 rabbitTransactionManager 事务管理器

rabbitTemplate 包含如下5种:

  • 发送普通字符串消息:rabbitTemplate
  • 发送json 消息:jsonRabbitTemplate(fastjson)
  • 发送json消息:jacksonRabbitTemplate(jackson)
  • 发送ack为true 的fastjson消息:ackRabbitTemplate
  • 有事务管理,发送fastjson 消息: transactionRabbitTemplate

4.6.9 基本字符串消息发送demo

见BasicPublisher 样例

4.6.10 json消息发送demo

见 JsonPublisher 样例

4.6.11 需要ack消息发送demo

见 AckPublisher 样例

4.6.12 事务管理模式消息发送demo

见TransactionPublisher样例

4.6.13 消息消费demo

见BasicConsumer 样例

4.7 集成操作日志

该模块的作用是,针对请求的action或者说controller资源,进行请求报文和响应报文的日志记录,以便于以后跟踪问题使用。

4.7.1 添加日志模块依赖

    <dependency>
        <groupId>cn.smilehappiness</groupId>
        <artifactId>smilehappiness-operation-log</artifactId>
    </dependency>
  • 1
  • 2
  • 3
  • 4

4.7.2 创建日志记录表

考虑到后续业务会越来越大,数据信息会越来越多,日志信息不再统一处理,而是把日志信息分别记录到每个业务系统下(业务拆分)。

创建表结构:

CREATE TABLE `api_logger` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `user_name` varchar(128) DEFAULT NULL COMMENT '用户名',
  `request_url` varchar(128) DEFAULT NULL COMMENT '请求url',
  `method_name` varchar(64) DEFAULT NULL COMMENT '请求方法名',
  `class_name` varchar(256) DEFAULT NULL COMMENT '请求类名',
  `request_type` varchar(32) DEFAULT NULL COMMENT '请求方式',
  `biz_id` varchar(64) DEFAULT NULL COMMENT '业务id,可为空,用户系统中,存储申请单id',
  `business_module_name` varchar(64) DEFAULT NULL COMMENT '请求的业务模块名称',
  `operation_describe` varchar(64) DEFAULT NULL COMMENT '操作描述',
  `request_params` text COMMENT '请求参数',
  `request_ip` varchar(64) DEFAULT NULL COMMENT '访问ip',
  `response_str` text COMMENT '响应结果',
  `operation_take_time` int(11) DEFAULT NULL COMMENT '接口调用耗时时间:单位毫秒',
  `error_message` varchar(256) DEFAULT NULL COMMENT '执行错误信息',
  `created_by` varchar(50) DEFAULT NULL COMMENT '创建人',
  `created_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  `updated_by` varchar(50) DEFAULT NULL COMMENT '修改人',
  `updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `delete_by` varchar(50) DEFAULT NULL COMMENT '删除人',
  `delete_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '删除时间',
  `is_delete` tinyint(2) DEFAULT '0' COMMENT '是否删除',
  `version` int(11) DEFAULT '1' COMMENT '乐观锁版本号',
  `remark` varchar(30) DEFAULT NULL COMMENT '备注信息',
  `udf_1` varchar(50) DEFAULT NULL COMMENT '扩展字段',
  `udf_2` varchar(50) DEFAULT NULL COMMENT '扩展字段',
  `udf_3` varchar(50) DEFAULT NULL COMMENT '扩展字段',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_biz_id` (`biz_id`),
  KEY `idx_request_url` (`request_url`),
  KEY `idx_business_module_name` (`business_module_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='操作日志记录';
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

4.7.3 如何使用

使用的时候非常简单,在需要记录日志的action地方,添加@OperateLog("方法的中文描述")
,即可实现基础日志信息的搜集处理。基础的信息包括:请求url、方法名称、类名、请求方式、方法的操作中文信息描述、请求参数、请求ip、响应参数、方法执行耗时多少毫秒、错误信息描述等等。

注: 保留了user_id、user_name、biz_id、business_module_name 四个字段,后续可以添加用户的信息、业务id以及业务模块名称,日后一旦系统有问题,核实问题可以有依有据。

具体使用示例,可参考如下代码:

	@OperateLog("通过请求url和方法名称,获取日志信息列表")
    @GetMapping("/getApiLoggerInfoByRequestUrlAndMethodName")
    public CommonResult<List<ApiLogger>> getApiLoggerInfoByRequestUrlAndMethodName(@RequestParam("requestUrl") String requestUrl, @RequestParam("methodName") String methodName) {
    return xxx;
}
  • 1
  • 2
  • 3
  • 4
  • 5

4.7.4 日志信息的保存

保存业务日志信息时,需要实现 IOperateLogStore 接口,然后覆盖父类的store方法即可

参考代码示例:

/**
 * <p>
 * 操作日志记录 服务实现类
 * </p>
 *
 * @author smilehappiness
 * @since 2021-10-02
 */
@Service
public class ApiLoggerServiceImpl extends ServiceImpl<ApiLoggerMapper, ApiLogger> implements ApiLoggerService, IOperateLogStore {

    /**
     * <p>
     * 存储操作日志信息
     * <p/>
     *
     * @param logList
     * @return void
     * @Date 2021/10/2 18:25
     */
    @Override
    public void store(List<OperateLogBaseInfo> logList) {
        //保存日志信息
        List<ApiLogger> apiLoggerList = DozerUtil.transForList(logList, ApiLogger.class);
        //注:这里直接操作性能不是很高,建议在mapper.xml进行批量保存
        saveBatch(apiLoggerList);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

4.8 集成定时任务管理模块

项目中,肯定会有定时任务跑批的使用场景,这里笔者使用了比较流行的 xxl-job 进行定时任务的管理。

4.8.1 添加定时任务模块依赖

    <dependency>
        <groupId>cn.smilehappiness</groupId>
        <artifactId>smilehappiness-schedule</artifactId>
    </dependency>
  • 1
  • 2
  • 3
  • 4

4.8.2 添加xxl-job配置

使用该定时任务模块时,需要在自己的yml或者properties配置文件中,添加xxl-job的配置,yml中添加配置示例如下:

xxl:
  job:
    ### 执行器通讯TOKEN [选填]:非空时启用;
    accessToken:
    admin:
      ### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
      # 这里的9999端口与部署的xxl-job-admin服务配置的端口一致即可
      addresses: http://localhost:9999/xxl-job-admin
    executor:
      ### 执行器地址
      address:
      ### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
      appname: smilehappiness-spring-cloud
      ### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
      ip:
      ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
      port: 1235
      ### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
      logpath: /usr/local/logs/smilehappiness-spring-cloud-server/xxl-job
      ### 执行器日志保存天数 [选填] :值大于3时生效,启用执行器Log文件定期清理功能,否则不生效;
      logretentiondays: 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

注:

  • addresses地址,根据实际情况进行配置,注意发布生产时,不要忘记切换生产的地址
  • appname改为当前项目的名称即可,比如用户系统,就是用的是:smile-user
  • port,xxl-job端口需要指定一个,并且不能跟其他的端口冲突,否则可能端口冲突错误
  • logpath 日志文件地址需要修改为自己的服务路径,比如使用:/data/logs/projectName-server/xxl-job
    ,用户系统使用的是:logpath: /data/logs/user-server/xxl-job

4.8.3 使用xxl-job

具体使用示例,可参考如下代码:

   /**
 * <p>
 * 操作日志记录 前端控制器
 * </p>
 *
 * @author smilehappiness
 * @since 2021-8-02
 */
@Slf4j
@Component
public class RiskCreditJob {

    /**
     * <p>
     * 数量统计job
     * <p/>
     *
     * @param
     * @return void
     * @Date 2021/10/3 14:42
     */
    @XxlJob(value = "creditCountStatistic")
    public void creditCountStatistic() throws Exception {
        try {
            // 获取参数
            String param = XxlJobHelper.getJobParam();
            log.info("数量统计job执行时,参数:{}", param);
            XxlJobHelper.log("数量统计job执行时,参数:{}", param);

            //TODO 执行业务方法。。。

            log.info("数量统计job执行成功!");
            XxlJobHelper.log("数量统计job执行成功!");

            XxlJobHelper.handleSuccess("数量统计job执行成功!");
        } catch (Exception e) {
            XxlJobHelper.log("数量统计job执行失败,失败原因:{}", e.getMessage());
            XxlJobHelper.handleFail("数量统计job执行失败,失败原因:" + e.getMessage());
        }
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

低版本使用以下示例:

    @XxlJob(value = "testJob")
    public void testJob() throws Exception {
        try {
            // 获取参数
            String param = XxlJobHelper.getJobParam();
            
            //TODO 执行业务方法。。。

            XxlJobHelper.log("参数记录。。。");
            XxlJobHelper.handleSuccess("业务方法执行成功!");
        } catch (Exception e) {
            XxlJobHelper.log("业务方法执行失败,失败原因:{}", e.getMessage());
            XxlJobHelper.handleFail("业务方法执行失败,失败原因:" + e.getMessage());
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

4.9 集成Swagger-UI管理模块

项目开发中,肯定会有接口文档和接口调试,为了更好地管理项目接口文档,那么,Swagger-UI绝对是一个很好用的工具。虽然Swagger-UI有在线文档,但是实际上,还是有一个专门的接口文档服务,文档看起来更加直观,后续可以使用Swagger-UI + 接口文档服务组合,可以更好地管理项目接口文档

4.9.1 添加Swagger-UI模块依赖

    <dependency>
        <groupId>cn.smilehappiness</groupId>
        <artifactId>smilehappiness-swagger-ui</artifactId>
    </dependency>
  • 1
  • 2
  • 3
  • 4

4.9.2 添加Swagger-UI配置

在yml或者properties配置文件中添加以下配置:

swagger:
  groupName: ${spring.application.name}
  enabled: true
  title: 后台接口文档
  base-package: cn.smilehappiness
  description: 后台管理框架
  license: Apache License, Version 2.0
  license-url: https://www.apache.org/licenses/LICENSE-2.0.html
  #接口文档地址,后续可以链接接口文档服务地址
  terms-of-service-url: http://localhost:6666/doc.html
  contact: xxx@163.com
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

注:

  • groupName 建议设置为自己的项目名称
  • enabled 这个开关,因为生产环境是面向客户,为了程序的安全性,在生产环境需要关闭,
  • contact 联系人可以填写负责该系统的联系人邮箱

4.9.3 使用Swagger-UI

Swagger-UI核心注解如下:

常用注解说明
@Api修饰整个类,描述Controller的作用
@ApiOperation描述一个类的一个方法,或者说一个接口
@ApiParam单个参数描述
@ApiModel用对象来接收参数
@ApiProperty用对象接收参数时,描述对象的一个字段
@ApiResponseHTTP响应其中1个描述
@ApiResponsesHTTP响应整体描述
@ApiIgnore使用该注解忽略这个API
@ApiError发生错误返回的信息
@ApiImplicitParam一个请求参数
@ApiImplicitParams多个请求参数

4.9.4 使用Swagger-UI代码示例

Swagger-UI使用示例如下:

/**
 * <p>
 * 操作日志记录 前端控制器
 * </p>
 *
 * @author smilehappiness
 * @since 2021-10-02
 */
@Slf4j
@Api(value = "ApiLoggerController", tags = "ApiLoggerController服务")
@RestController
@RequestMapping("/apiLogger")
public class ApiLoggerController {

    private ApiLoggerService apiLoggerService;

    public ApiLoggerController(ApiLoggerService apiLoggerService) {
        this.apiLoggerService = apiLoggerService;
    }

    /**
     * <p>
     * 通过请求url和方法名称,获取日志信息列表
     * <p/>
     *
     * @param requestUrl
     * @param methodName
     * @return CommonResult<List < ApiLogger>>
     * @Date 2021/10/4 11:46
     */
    @ApiOperation(notes = "通过请求url和方法名称,获取日志信息列表", value = "getApiLoggerInfoByRequestUrlAndMethodName")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "requestUrl", value = "请求url", type = "String"),
            @ApiImplicitParam(name = "methodName", value = "方法名称", type = "String")
    })
    @ApiResponses({
            @ApiResponse(code = 200, message = "通过请求url和方法名称,获取日志信息列表成功"),
            @ApiResponse(code = 400, message = "请求参数有误"),
            @ApiResponse(code = 500, message = "服务器内部异常,请稍后再试")
    })
    @OperateLog("通过请求url和方法名称,获取日志信息列表")
    @GetMapping("/getApiLoggerInfoByRequestUrlAndMethodName")
    public CommonResult<List<ApiLogger>> getApiLoggerInfoByRequestUrlAndMethodName(@RequestParam("requestUrl") String requestUrl, @RequestParam("methodName") String methodName) {
        LocalDateTime bizTimeStart = LocalDateTime.now();
        CommonResult<List<ApiLogger>> commonResult = new CommonResult<>();
        try {
            List<ApiLogger> apiLoggerList = apiLoggerService.getApiLoggerInfoByRequestUrlAndMethodName(requestUrl, methodName);
            if (CollectionUtils.isEmpty(apiLoggerList)) {
                throw new BusinessException(FrameworkBusinessExceptionEnum.API_LOGGER_INFO_NULL);
            }

            log.info("通过请求url和方法名称,获取日志信息接口返回结果:{}", JSON.toJSONString(commonResult));
            log.info("通过请求url和方法名称,获取日志信息方法执行耗时(毫秒):{}", DateUtil.getTakeTime(bizTimeStart, LocalDateTime.now(), TimeUnit.MILLISECONDS));

            return commonResult;
        } catch (BusinessException e) {
            log.error("【业务异常】通过请求url和方法名称,获取日志信息异常,异常原因:{}", e.getMessage());
            log.info("通过请求url和方法名称,获取日志信息方法执行耗时(毫秒):{}", DateUtil.getTakeTime(bizTimeStart, LocalDateTime.now(), TimeUnit.MILLISECONDS));

            throw new BusinessException(e.getCode(), e.getBizCode(), "通过请求url和方法名称,获取日志信息异常,异常原因:" + e.getMessage());
        } catch (Exception e) {
            log.error("【系统异常】通过请求url和方法名称,获取日志信息异常,异常原因:{}", e.getMessage());
            log.info("通过请求url和方法名称,获取日志信息方法执行耗时(毫秒):{}", DateUtil.getTakeTime(bizTimeStart, LocalDateTime.now(), TimeUnit.MILLISECONDS));

            throw new SystemInternalException("通过请求url和方法名称,获取日志信息异常,异常原因:" + e.getMessage());
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

4.10 集成分布式id

一般情况下,使用Mysql数据库自增id就满足使用了,但是如果涉及到数据迁移,或者后面数据量可能比较大,会涉及到分库分表,那么使用分布式id,后续整体的维护更加方便。

注:这里集成的是百度的分布式id,可以集成在项目中使用,免去搭建分布式id服务的麻烦。

4.10.1 需要创建数据库表,来区分服务的id


CREATE TABLE `WORKER_NODE` (
  `ID` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'auto increment id',
  `HOST_NAME` varchar(64) NOT NULL COMMENT 'host name',
  `PORT` varchar(64) NOT NULL COMMENT 'port',
  `TYPE` int(11) NOT NULL COMMENT 'node type: ACTUAL or CONTAINER',
  `LAUNCH_DATE` date NOT NULL COMMENT 'launch date',
  `CREATED` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',
  `MODIFIED` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'modified time',
  PRIMARY KEY (`ID`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='DB WorkerID Assigner for UID Generator';

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

4.10.2 添加依赖

  <dependency>
      <groupId>cn.smilehappiness</groupId>
      <artifactId>smilehappiness-distribute-id</artifactId>
  </dependency>
  • 1
  • 2
  • 3
  • 4

4.10.3 entity或者model实体类,继承BaseVo基类

注:目前BaseVo没有统一,暂时由各个服务自己维护

@Data
public class BaseVo<T> implements Serializable {

	//Specify the primary key generation strategy to use the snowflake algorithm (default strategy )
    //The snowflake algorithm (snowflake) is a distributed ID generation algorithm open source on Weibo. Its core idea is to use a 64-bit long number as the global unique ID. It is widely used in distributed systems, and the ID introduces a timestamp, which basically keeps self-increasing 。
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private Date createdTime;

    /**
     * 创建人 格式 账号名/登录用户名
     */
    private String createdBy;

    /**
     * 修改时间
     */
    @TableField(fill = FieldFill.UPDATE)
    private Date updatedTime;

    /**
     * 修改人 格式 账号名/登录用户名
     */
    @TableField(fill = FieldFill.UPDATE)
    private String updatedBy;

    @TableLogic
    private boolean isDelete;

    /**
     * 乐观锁版本号
     */
    private String version;

    /**
     * 备注
     */
    private String remark;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

4.10.4 引入分布式id数据源

注:不同的环境下,数据库url、username、password不一样

spring:
  datasource:
      dynamic:
        # 设置默认的数据源或者数据源组,默认值即为master
        primary: master
        # 严格匹配数据源,默认false. true未匹配到指定数据源时抛异常,false使用默认数据源
        strict: false
        # 默认false非懒启动,系统加载到数据源立即初始化连接池
        lazy: false
        # 全局hikariCP参数,所有值和默认保持一致(现已支持的参数如下)
        hikari:
          catalog:
          # 数据库连接超时时间,默认30秒,即 30000
          connection-timeout: 30000
          validation-timeout:
          #空闲连接存活最大时间,默认 600000(10分钟)
          idle-timeout: 600000
          leak-detection-threshold:
          max-lifetime:
          #连接池最大连接数,默认是10
          max-pool-size: 10
          #最小空闲连接数量
          min-idle: 10
          initialization-fail-timeout:
          connection-init-sql:
          connection-test-query:
          dataSource-class-name:
          dataSource-jndi-name:
          schema:
          transaction-isolation-name:
          # 此属性控制从池返回的连接的默认自动提交行为,默认值:true
          is-auto-commit: true
          is-read-only: false
          is-isolate-internal-queries:
          is-register-mbeans:
          is-allow-pool-suspension:
          data-source-properties:
          health-check-properties:
        datasource:
          master:
            type: com.zaxxer.hikari.HikariDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://ip:13306/db?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&allowMultiQueries=true
            username: root
            password: password
            # 以下参数针对每个库可以重新设置hikari参数
            hikari:
              max-pool-size: 20
              idle-timeout: 120000
          distribute:
            type: com.zaxxer.hikari.HikariDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://ip:13306/smile_distribute_id?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Jakarta
            username: root
            password: password
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

4.10.5 自动注入分布式id

在BaseVo中的id,设置@TableId(type = IdType.ASSIGN_ID),然后使用下面的代码,即可实现自动分布式id


package cn.smilehappiness.distribute.config;

import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import cn.smilehappiness.distribute.service.impl.CachedUidGenerator;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

import javax.annotation.Resource;


/**
 * <p>
 * Integrated distributed id
 * The numeric type does not support automatic conversion, and needs precise matching. For example, if you return Long, the entity primary key cannot be defined as Integer
 * <p/>
 *
 * @author
 * @Date 2021/12/8 21:11
 */
@Configuration
public class DistributeIdGeneratorConfig implements IdentifierGenerator {

    @Lazy
    @Resource
    private CachedUidGenerator cachedUidGenerator;

    @Override
    public Long nextId(Object entity) {
        //Call the distributed ID service, generate the distributed ID, and return the generated ID value 
        return cachedUidGenerator.getUID();
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

4.10.6 还可以支持手动设置分布式id

上面实现的是自动注入分布式id,如果需要手动注入,可以使用下面的方式,在插入数据之前,手动设置主键id的值即可


import cn.smilehappiness.distribute.service.impl.CachedUidGenerator;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * @author
 * @since : 2021/12/8 15:58:26
 */
@Component
public class UidUtil {

    @Resource
    private CachedUidGenerator cachedUidGenerator;

    private static CachedUidGenerator staticUid;

    @PostConstruct
    public void init(){
        staticUid = cachedUidGenerator;
    }

    public static long id(){
        return staticUid.getUID();
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

4.11 国际化功能

4.11.1 前言

国际化(internationalization),又称为i18n。对于某些应用系统而言,它需要发布到不同的国家地区,因此需要特殊的做法来支持,也即是国际化。通过国际化的方式,实现界面信息,各种提示信息等内容根据不同国家地区灵活展示的效果。比如在中国,系统以简体中文进行展示,在美国则以美式英文进行展示。如果使用传统的硬编码方式,是无法做到国际化支持的。

所以通俗来讲,国际化就是为每种语言配置一套单独的资源文件,保存在项目中,由系统根据客户端需要选择合适的资源文件。

国际化功能设计图:
在这里插入图片描述

i18n:国际化,因为这个单词从i到n有18个英文字母,因此命名

注:该国际化是基于nacos动态配置实现的,另外,在git ignore中添加 i18n/,忽略文件提交

4.11.2 Spring Boot的国际化支持

  SpringBoot默认提供了国际化的支持,它通过自动配置类`MessageSourceAutoConfiguration`实现。
  • 1

在SpringBoot代码中实现原生国际化配置仅需要以下三步:

  • 指定国际化资源路径
    通过application.properties指定:spring.messages.basename=i18n/,通过nacos获取配置信息,配置下发路径,会通过代码方式,从nacos重新下发到服务路径中,其中,spring.messages.basename的i18n表示resources路径上的一个文件夹,messages就是这个文件夹下的资源文件名,例如:messages_in_ID.properties(印尼)、messages_zh_CN.properties (中文)、messages_en_US.properties (英文)等。

  • 注入国际化Resolver对象
    通过指定LocaleResolver对象,实现国际化策略

在实际工作中,我们应该且有必要对国际化做进一步的增强,让它更能满足要求。基于上述的问题,我们做了一些改进,最终达到的效果如下:

  • 配置中心存储应用的国际化配置,配置支持动态刷新,实时生效
  • 实现高效的配置读取
  • 简化前后端的工作量

4.11.3 如果配置多语言内容

配置了多语言之后,再跑业务信息时,可以动态刷新,返回不同国家的提示信息。

注:格式严格遵从properties标准,且每个多语言文件都要添加,每次都要主动检查,防止忘记添加,导致未查询到语言描述,防止返回不正常业务描述信息。

4.11.4 国际化对照表

语言代码国家/ 地区
“”(空字符串) 无变化的文化
zh-CN华 -中国
en-US英国 - 美国
id-ID印尼 -印尼

注:日常使用过程中,发现有不少的地方会提示使用in-ID,这个应该是历史原因造成的,相关资料:https://www.itbaoku.cn/post/2125139/do

4.11.5 引用多语言国际化依赖包

      <dependency>
             <groupId>cn.smilehappiness</groupId>
              <artifactId>smilehappiness-language</artifactId>
       </dependency>
  • 1
  • 2
  • 3
  • 4

4.11.6 添加nacos配置信息

在每个所依赖的项目中,添加nacos配置信息

spring:  
  messages:
    ## 指定国际化配置存放的本地路径(在程序的当前路径下的路径)
    baseFolder: i18n/
    ## 国际化配置名称
    basename: messages
    ## 国际化配置名称
    encoding: UTF-8
    ## 国际化本地配置刷新的时间间隔
    cacheMillis: 15000
    ## 全系统默认返回的国际化语言
    defaultLang: in_ID
    ## 支持多语言配置
    langList:
      - in_ID
      - zh_CN
      - en_US

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

4.11.7 基于脚手架如何快速使用国际化

在Nacos上新增应用的国际化配置,命名空间选择 “提示语”,Data ID为(messages+国别):

推荐方式:
messages_in_ID,messages_zh_CN,messages_en_US

也可以配置成这样,(但是需要配置 spring.messages.fileSuffix=.properties)
messages_in_ID.properties,messages_zh_CN.properties,messages_en_US.properties

这样子,在启动项目的时候,会自动拉取这样的语言包,作为本地缓存,也会定时去刷新语言包缓存。可以基于自己的业务扩展不同的多语言配置。

注入工具类:

    @Resource
    private I18nUtil i18nUtil;
  • 1
  • 2

demo: 启动后请求 http://localhost:8010/langByKey

在http接口中,需要加:
header:Accept-Language
value:in_ID

或者

这个优先级高于前者
header:Lang
value:in_ID

取单个描述,返回字符串:

i18nUtil.getString(langKey)
  • 1

取多个,返回json

i18nUtil.getString("a","b")
  • 1

获取全部,返回全部json

i18nUtil.getString()
  • 1

5 其他功能处理

5.1 项目中如何使用分页功能

分页功能,在运营后台管理界面,使用的比较多,目前,该分页功能支持基础的分页功能,也支持自定义sql实现分页

分页功能里面,集成了Mybatis-Plus分页插件功能,为了兼容老项目,也集成了PageHelper分页插件

5.1.1 集成Mybatis-Plus分页功能

该分页功能,可以设置当前页当前页获取多少条数据,另外,增加了排序字段升序还是降序的功能。

注: 排序字段可以有多个,如果有多个,以逗号分割,比如:user_name,created_time

5.1.1.1 使用Mybatis-Plus中baseMapper的分页功能
  • 首先定义一个请求参数dto,继承PageRequestDto,比如获取用户分页信息时,定义一个请求dto,后续可以支持各种条件的筛选
/**
 * <p>
 * 用户系统页面请求参数
 * <p/>
 *
 * @author lijunwei
 * @Date 2021/10/9 19:48
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class UserRequestDto extends PageRequestDto {
	//自己生成一个序列化id
    private static final long serialVersionUID = xxx;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 定义controller
/**
     * <p>
     * 获取用户信息分页列表
     * <p/>创建表结构
     *
     * @param userRequestDto
     * @return 
     * @Date 2021/10/10 11:02
     */
    @ApiOperation(notes = "获取用户信息分页列表", value = "queryUserInfoPageList")
    @OperateLog("获取用户信息分页列表")
    @GetMapping("/queryUserInfoPageList")
    public ObjectRestResponse<PageResultResponse<User>> queryUserInfoPageList(@ModelAttribute UserRequestDto userRequestDto) {
        IPage<User> iPage = userService.queryUserInfoPageList(userRequestDto);
        return success(this.toPageResultResponse(iPage));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 定义service和实现类
 /**
     * <p>
     * 获取用户信息分页列表
     * <p/>
     *
     * @param userRequestDto
     * @return com.baomidou.mybatisplus.core.metadata.IPage<xxx.User>
     * @Date 2021/10/9 20:13
     */
    @Override
    public IPage<User> queryUserInfoPageList(UserRequestDto userRequestDto) {
        Page<User> page = new Page<>(userRequestDto.getCurrent(), userRequestDto.getSize());
        //执行查询条件
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.orderBy(StringUtils.isNotEmpty(userRequestDto.getSortFields()), userRequestDto.isAsc(), userRequestDto.getSortFields());
        return baseMapper.selectPage(page, queryWrapper);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

以上简单操作,就可以实现基础的分页功能

5.1.1.2 自定义mapper实现分页功能
  • service实现示例
/**
     * <p>
     * 获取用户信息分页列表-xml自定义sql
     * <p/>
     *
     * @param userRequestDto
     * @return com.baomidou.mybatisplus.core.metadata.IPage<xxx.User>
     * @Date 2021/10/10 11:03
     */
    @Override
    public IPage<User> queryUserInfoPageListMapper(UserRequestDto userRequestDto) {
        Page<User> page = new Page<>(userRequestDto.getCurrent(), userRequestDto.getSize());
        return baseMapper.queryUserInfoPageListMapper(page, userRequestDto);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • mapper实现示例
 /**
     * <p>
     * 获取用户信息分页列表-xml自定义sql
     * <p/>
     *
     * @param page
     * @param userRequestDto
     * @return com.baomidou.mybatisplus.core.metadata.IPage<xxx.User>
     * @Date 2021/10/10 11:04
     */
    IPage<User> queryUserInfoPageListMapper(Page<User> page, @Param("userRequestDto") UserRequestDto userRequestDto);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • mapper.xml实现示例
  <select id="queryUserInfoPageListMapper" parameterType="xxx.UserRequestDto"
            resultType="xxx.User">
        SELECT
        <include refid="Base_Column_List"/>
        FROM user WHERE is_delete = 0
        <if test="userRequestDto.sortFields != null  and userRequestDto.sortFields !='' ">
            order by ${userRequestDto.sortFields}

            <choose>
                <when test="userRequestDto.isAsc != null and userRequestDto.isAsc == true">
                    asc
                </when>
                <otherwise>
                    desc
                </otherwise>
            </choose>
        </if>
    </select>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

5.1.2 使用PageHelper分页功能

无需添加任何配置,直接就可以使用,代码示例如下:

  • 定义controller
/**
     * <p>
     * 获取日志信息分页列表,基于pagehelper,这里PageQueryRequest参数的封装为了兼容老系统的使用
     * <p/>
     *
     * @param request
     * @return cn.smilehappiness.result.ObjectRestResponse<cn.smilehappiness.common.page.PageResultResponse < cn.smilehappiness.xxx.ApiLogger>>
     * @Date 2021/10/10 19:01
     */
    @ApiOperation(notes = "获取日志信息分页列表", value = "getApiLoggerPageList")
    @PostMapping("/getApiLoggerPageList")
    public ObjectRestResponse<PageResultResponse<ApiLogger>> getApiLoggerPageList(@RequestBody PageQueryRequest<ApiLoggerRequestDto> request) {
        PageResultResponse<ApiLogger> apiLoggerPageList = apiLoggerService.getApiLoggerPageList(request);
        return success(apiLoggerPageList);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 定义service和实现类
 /**
     * <p>
     * 获取日志信息分页列表
     * <p/>
     *
     * @param request
     * @return cn.smilehappiness.common.page.PageResultResponse<xxx.ApiLogger>
     * @Date 2021/10/10 19:00
     */
    @Override
    public PageResultResponse<ApiLogger> getApiLoggerPageList(PageQueryRequest<ApiLoggerRequestDto> request) {
        PageUtil.startPage(request);

        //获取数据
        QueryWrapper<ApiLogger> ew = new QueryWrapper<>();
        if (request.getT() != null) {
            ApiLoggerRequestDto apiLoggerRequestDto = request.getT();
            if (StringUtils.isNotBlank(apiLoggerRequestDto.getBizId())) {
                ew.eq("biz_id", apiLoggerRequestDto.getBizId());
            }
        }
        ew.orderByDesc("created_time");
        List<ApiLogger> list = baseMapper.selectList(ew);

        // 取分页信息
        PageInfo<ApiLogger> pageInfo = new PageInfo<>(list);
        return PageUtil.toPageResponse(pageInfo);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

5.2 接口幂等性处理

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

接口幂等性是一个很常见的问题,用户发起一次表单提交时,如果在短时间内发起了多次提交,这时候如果处理不当,就会引起脏数据,所以,接口的幂等性处理,还是很有必要的。

5.2.1 校验思路以及代码示例

基于用户的token,以及需要限制的controller资源的唯一key,进行时间周期内的限制处理,如果多次提交,禁止访问,从而达到合法请求的效果。

代码示例,自定义注解:

package cn.smilehappiness.cache.interceptor;

import java.lang.annotation.*;

/**
 * <p>
 * Interface idempotence verification annotation 
 * <p/>
 *
 * @author
 * @Date 2021/10/14 15:17
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface InterfaceIdempotency {

    /**
     * Operation Token validity period, unit: seconds, default: 90 seconds 
     */
    int operationTokenExpire() default 90;

    /**
     * TokenPrompt message on failure 
     */
    String errorMessage() default "This operation has been submitted, please do not resubmit ";

    /**
     * The unique key of the form can only be submitted once within the valid time of the same key to ensure the idempotence of the interface 
     */
    String formUniqueKey() default "formUniqueKey";

    /**
     * Whether to force verification of request headers and token validity 
     *
     * @return true or false
     */
    boolean validateToken() default true;

}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

拦截器实现:

package cn.smilehappiness.cache.interceptor;

import cn.smilehappiness.cache.util.RedisUtil;
import cn.smilehappiness.cache.util.RedissonLockRedisUtil;
import cn.smilehappiness.common.enums.ResultCodeEnum;
import cn.smilehappiness.exception.exceptions.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * <p>
 * Idempotent interceptor processing 
 * <p/>
 *
 * @author
 * @Date 2021/10/14 15:34
 */
@Slf4j
@Component
public class IdempotencyInterceptor extends HandlerInterceptorAdapter {

    private static final String IDEMPOTENCY_KEY = "idempotencyKey";
    private static final String LOCK_OBJ = "lockObj";
    private static final String IDEMPOTENCY_PREFIX = "interfaceOperation:idempotency:";
    public static final String HEAD_AUTHORIZATION = "Authorization";
    /**
     * TokenPrompt message on failure 
     */
    private static final String ERROR_MESSAGE = "This operation has expired. Please refresh and try again ";

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private RedissonLockRedisUtil redissonLockRedisUtil;
    @Resource
    private RedissonClient redissonClient;


    /**
     * <p>
     * Idempotency interceptor preprocessing, note: if token is empty, idempotency verification is not performed 
     * <p/>
     *
     * @param request
     * @param response
     * @param handler
     * @return boolean
     * @Date 2021/10/14 15:56
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        HandlerMethod handlerMethod = (HandlerMethod) handler;
        InterfaceIdempotency idempotency = handlerMethod.getMethodAnnotation(InterfaceIdempotency.class);
        if (idempotency == null) {
            return true;
        }

        if (redisUtil == null) {
            log.warn("RedisUtil injection failed during interface idempotence verification ");
            return true;
        }

        String token = request.getHeader(HEAD_AUTHORIZATION);
        if (StringUtils.isBlank(token)) {
            if (idempotency.validateToken()) {
                log.warn("Idempotency check interface: {}, token in request header is empty .", request.getRequestURI());
                //response.sendError(Integer.parseInt(ResultCodeEnum.UNAUTHORIZED.getCode()), ERROR_MESSAGE);
                throw new BusinessException(ResultCodeEnum.UNAUTHORIZED.getCode(), "@@IDEMPOTENCY_TOKEN_EXPIRE##", ERROR_MESSAGE);
            }
            return true;
        }

        String idempotencyKey = StringUtils.join(IDEMPOTENCY_PREFIX, idempotency.formUniqueKey(), ":", token);

        //It supports the expiration unlocking function. It will be automatically unlocked after 90 seconds without calling the unlock method. Of course, in order not to occupy resources and use the lock to handle the business, it is generally recommended to release the lock manually 
        //RLock lock = redissonLockRedisUtil.lock(idempotencyKey, idempotency.operationTokenExpire());

        RLock lock = redissonClient.getLock(idempotencyKey);

        request.setAttribute(IDEMPOTENCY_KEY, idempotencyKey);
        request.setAttribute(LOCK_OBJ, lock);

        //1.0.7Release the lock manually (improve performance )
        if (lock.tryLock()) {
            //execute biz method
            log.info("During interface idempotence verification, key: [{}], [{}] verification passed ", idempotencyKey, idempotency.formUniqueKey());
            return true;
        } else {
            log.warn("During interface idempotency verification, key: [{}], [{}] forms are submitted repeatedly ", idempotencyKey, idempotency.formUniqueKey());
            throw new BusinessException(ResultCodeEnum.SERVICE_UNAVAILABLE.getCode(), "@@FORM_REPEAT_COMMIT##", idempotency.errorMessage());
        }
    }

    /**
     * This implementation is empty.
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                @Nullable Exception ex) throws Exception {
        if (handler instanceof HandlerMethod && request.getAttribute(LOCK_OBJ) != null) {
            RLock lock = (RLock) request.getAttribute(LOCK_OBJ);
            //If a lock exists, release the lock 
            if (lock.isLocked()) {
                lock.unlock();
            }
        }
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126

注:幂等性是基于user的token处理的,对于非用户维度的接口,或者没有token的接口,无法使用该幂等校验方式。另外,如果对于某个接口,需要限定多少时间内,接口请求限制,需要其他额外处理

5.2.2 基础接口幂等校验的使用

在需要使用的action上,加上 @InterfaceIdempotency(formUniqueKey = "模块名:根据action的方法名称组合的唯一key"),就可以实现基础的接口幂等校验。

@InterfaceIdempotency参数说明:

  • operationTokenExpire 标识过期时间,默认90秒,如果该次请求没有返回结果,90秒内不允许再次提交表单
  • errorMessage 用户多次提交时,默认提示:该操作已提交,请勿重复提交
  • formUniqueKey 标识需要做接口幂等校验的唯一key,如果不填写,默认为:interfaceOperation:idempotency:formUniqueKey

示例: @InterfaceIdempotency(formUniqueKey = "user:addUserBankInfo")

5.3 集成Skywalking分布式链路追踪

Skywalking的特点是,在没有侵入的情况下集成日志监控,实现起来也很简单。要使得项目集成日志监控功能,`主要依靠的就是使用

javaagent参数集成skywalking的agent服务功能,简而言之就是启动项目时,同时启动skywalking-agent.jar`这个服务,多个项目都可以

共用一个skywalking-agent.jar,这个jar就在安装目录下的agent目录中,不同的项目要对agent.config文件做单独配置,主要就是

配置应用名称。为了方便易用,skywalking支持使用参数实现配置覆盖

优先级:探针方式>系统配置方式

5.3.1 系统配置方式

使用 -D参数设置应用名称,skywalking.agent.service_name是属性,=后面是值;skywalking.collector.backend_service对应的是收集服务的地址

java -javaagent:/apache-skywalking-apm-bin/agent/skywalking-agent.jar
-Dskywalking.agent.service_name=app-service 
-Dskywalking.collector.backend_service=127.0.0.1:11800
-jar app-service.jar &
  • 1
  • 2
  • 3
  • 4

5.3.2 探针方式

在skywalking-agent.jar后直接追加 =agent.service_name=应用名称

java -javaagent:/apache-skywalking-apm-bin/agent/skywalking-agent.jar=agent.service_name=app-service -jar app-service.jar &
  • 1

5.3.3 插件使用

添加依赖:

 <dependency>
 	  <groupId>org.apache.skywalking</groupId>
      <artifactId>apm-toolkit-trace</artifactId>
  </dependency>
  • 1
  • 2
  • 3
  • 4

注:默认情况agent是不支持对spring-cloud-gateway的监控的,需要插件的支持。我们要将optional-plugins下的插件apm-spring-cloud-gateway-2.x-plugin-6.5.0.jar拷贝到plugins下,使agent可以加载到该插件,其他一些需要额外插件支持的中间件和框架也是同理操作。

5.3.4 traceId接入

  • 添加依赖

     <dependency>
         <groupId>org.apache.skywalking</groupId>
         <artifactId>apm-toolkit-logback-1.x</artifactId>
     </dependency>
    
    • 1
    • 2
    • 3
    • 4
  • logback文件修改

        <property name="CONSOLE_LOG_PATTERN"
                  value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr([%tid]) %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    
    
    • 1
    • 2
    • 3

    CONSOLE appender配置修改

            <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
                <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                    <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
                </layout>
                <charset>UTF-8</charset>
            </encoder>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    DEBUG及以上级别的appender 配置修改

            <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
                <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%tid] %-5level %logger{50} - %msg%n</pattern>
                </layout>
                <charset>UTF-8</charset> <!-- 设置字符集 -->
            </encoder>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

5.4 异步线程及Mq消费端日志追踪TID问题

Mq消费端日志无法打印TID信息? 业务代码中进行异步调用,无法追踪异步线程链路?不方便我们追踪日志,这里笔者也遇到过,这里也稍微总结一下:

  • MQ TID丢失问题
    在@RabbitListener上加上注解 @Trace

  • 异步线程TID丢失问题
    部署 apm-jdk-threading-plugin 插件,这个插件位于${skywalking_dir}/agent/bootstrap-plugins目录下,将其复制到${skywalking_dir}/agent/plugins目录下即可

    修改${skywalking_dir}/agent/config/agent.config文件,在其底部添加如下配置(基于实际包名调整):
    plugin.jdkthreading.threading_class_prefixes=cn.smilehappiness

5.5 脚手架初始化新项目

可以参考我另一篇博客:Java中如何快速构建项目脚手架

6 基础项目下载地址

6.1 基础功能架构项目下载地址

6.2 微服务脚手架项目下载地址

基础功能微服务架构,可以参考我上一篇的博客: Spring Cloud 微服务基础功能架构来啦

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家一起探讨,代码如有问题,欢迎各位大神指正!

给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/939341
推荐阅读
相关标签
  

闽ICP备14008679号