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

文章目录
1 前言
2 脚手架主要提供哪些功能
3 如何使用该脚手架
3.1 项目统一依赖管理
3.2 集成基础模块功能到自己的项目中
4 基础核心功能模块的使用
4.1 集成缓存管理模块
4.1.1 添加cache模块依赖
4.1.2 cache模块的功能使用
4.2 集成通知预警管理模块
4.2.1 添加通知预警模块依赖
4.2.2 添加yml配置
4.2.3 钉钉预警使用示例
4.3 集成异常管理模块
4.3.1 添加异常模块依赖
4.3.2 使用异常
4.3.2.1 两种异常说明
4.3.2.2 异常使用示例
4.4 集成限流管理模块
4.4.1 前言
4.4.2 添加限流管理模块依赖
4.4.3 实现的思路
4.4.4 如何使用该模块功能进行限流
4.4.5 代码示例
4.5 集成Mock Server管理模块
4.5.1 MockServer是什么
4.5.2 为什么要使用MockServer
4.5.3 Mock Server部署
4.5.4 Mock Server的使用
4.6 集成MQ管理模块
4.6.1 使用RabbitMQ
4.6.2 基本概念
4.6.3 特点
4.6.4 概念理解
4.6.4.1 Message
4.6.4.2 Publisher
4.6.4.3 Exchange
4.6.4.4 Binding
4.6.4.5 Queue
4.6.4.6 Connection
4.6.4.7 Channel
4.6.4.8 Consumer
4.6.4.9 Virtual Host
4.6.4.10 Broker
4.6.5 添加MQ模块依赖
4.6.6 配置MQ相关配置
4.6.7 使用demo
4.6.8 RabbitConfig 配置类解析
4.6.9 基本字符串消息发送demo
4.6.10 json消息发送demo
4.6.11 需要ack消息发送demo
4.6.12 事务管理模式消息发送demo
4.6.13 消息消费demo
4.7 集成操作日志
4.7.1 添加日志模块依赖
4.7.2 创建日志记录表
4.7.3 如何使用
4.7.4 日志信息的保存
4.8 集成定时任务管理模块
4.8.1 添加定时任务模块依赖
4.8.2 添加xxl-job配置
4.8.3 使用xxl-job
4.9 集成Swagger-UI管理模块
4.9.1 添加Swagger-UI模块依赖
4.9.2 添加Swagger-UI配置
4.9.3 使用Swagger-UI
4.9.4 使用Swagger-UI代码示例
4.10 集成分布式id
4.10.1 需要创建数据库表,来区分服务的id
4.10.2 添加依赖
4.10.3 entity或者model实体类,继承BaseVo基类
4.10.4 引入分布式id数据源
4.10.5 自动注入分布式id
4.10.6 还可以支持手动设置分布式id
4.11 国际化功能
4.11.1 前言
4.11.2 Spring Boot的国际化支持
4.11.3 如果配置多语言内容
4.11.4 国际化对照表
4.11.5 引用多语言国际化依赖包
4.11.6 添加nacos配置信息
4.11.7 基于脚手架如何快速使用国际化
5 其他功能处理
5.1 项目中如何使用分页功能
5.1.1 集成Mybatis-Plus分页功能
5.1.1.1 使用Mybatis-Plus中baseMapper的分页功能
5.1.1.2 自定义mapper实现分页功能
5.1.2 使用PageHelper分页功能
5.2 接口幂等性处理
5.2.1 校验思路以及代码示例
5.2.2 基础接口幂等校验的使用
5.3 集成Skywalking分布式链路追踪
5.3.1 系统配置方式
5.3.2 探针方式
5.3.3 插件使用
5.3.4 traceId接入
5.4 异步线程及Mq消费端日志追踪TID问题
5.5 脚手架初始化新项目
6 基础项目下载地址
6.1 基础功能架构项目下载地址
6.2 微服务脚手架项目下载地址
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 实现的思路
思路: 借助于Redis的INCR操作来实现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管理模块
在项目开发中,消息中间件服务用的也比较多,这里简单总结一下经常用到的两个消息中间件:RabbitMQ和RocketMQ。

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    用对象接收参数时,描述对象的一个字段
@ApiResponse    HTTP响应其中1个描述
@ApiResponses    HTTP响应整体描述
@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,进行时间周期内的限制处理,如果多次提交,禁止访问,从而达到合法请求的效果。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Cloud Alibaba是一个基于Spring Cloud微服务架构解决方案,它集成了多个阿里巴巴开源项目,包括Nacos、Sentinel、Dubbo等,提供了丰富的微服务组件和工具。 制作Spring Cloud Alibaba脚手架,可以按照以下步骤进行: 第一步,创建一个基于Spring Cloud的项目。可以使用Spring Initializr等工具创建一个Spring Boot项目,然后再引入Spring Cloud的相关依赖。 第二步,引入Spring Cloud Alibaba的依赖和配置。在项目的pom.xml文件中添加Spring Cloud Alibaba的相关依赖,包括Nacos、Sentinel等组件的依赖。 第三步,配置和启动Nacos注册中心。在项目的配置文件中配置Nacos的相关信息,包括注册中心地址、命名空间等。然后启动Nacos注册中心。 第四步,配置和启动其他Spring Cloud Alibaba组件。根据项目的需求,配置和启动其他Spring Cloud Alibaba组件,如Sentinel、Dubbo等。 第五步,编写微服务相关代码。根据项目的需求,编写微服务的相关代码,包括接口、服务实现等。 第六步,部署和测试。将项目打包成可执行的jar包,并部署到服务器上进行测试。可以使用Postman等工具测试接口的调用和服务的可用性。 通过以上步骤,我们可以制作出一个基于Spring Cloud Alibaba的脚手架,可以用来快速搭建微服务架构项目。制作好的脚手架可以提供给其他团队使用,并可以根据具体需求进行扩展和定制。脚手架的使用可以大大提高开发效率,减少重复工作,推动微服务架构的落地和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值