基于Spring Cloud 的微服务架构脚手架,满满的干货来啦~

Posted 緈諨の約錠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于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 微服务基础功能架构来啦 中的基础架构,搭建了一套脚手架,可以直接拿来使用。

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

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

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

3 如何使用该脚手架

3.1 项目统一依赖管理

  • 集成基础设施项目:

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

        <parent>
            <groupId>com.smilehappiness</groupId>
            <artifactId>smilehappiness-architecture</artifactId>
            <version>1.0.0</version>
        </parent>
    

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

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

	<dependency>
	   <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-cache</artifactId>
	    <exclusions>
	        <exclusion>
	            <artifactId>HdrHistogram</artifactId>
	            <groupId>org.hdrhistogram</groupId>
	        </exclusion>
	    </exclusions>
	</dependency>
	<dependency>
	    <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-common</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-early-warning-notice</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-exception</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-limit-api</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-mq</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-operation-log</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-schedule</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-swagger-ui</artifactId>
	</dependency>
	<dependency>
	    <groupId>com.smilehappiness</groupId>
	    <artifactId>smilehappiness-utils</artifactId>
	</dependency>

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

4.1 集成缓存管理模块

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

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

4.1.1 添加cache模块依赖

     <dependency>
          <groupId>com.smilehappiness</groupId>
          <artifactId>smilehappiness-cache</artifactId>
     </dependency>

4.1.2 cache模块的功能使用

  • Redis工具的基本使用示例

    注入以下工具类:

    	@Autowired
        private RedissonClient redissonClient;
        @Autowired
        private RedisUtil redisUtil;
        @Autowired
        private RedissonLockRedisUtil redissonLockRedisUtil;
    

    然后使用以下测试用例:

    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");
    }
    
  • 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("系统繁忙,请稍后再试!");
        }
    }
    

4.2 集成通知预警管理模块

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

4.2.1 添加通知预警模块依赖

     <dependency>
          <groupId>com.smilehappiness</groupId>
          <artifactId>smilehappiness-early-warning-notice</artifactId>
     </dependency>

4.2.2 添加yml配置

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

# 钉钉预警通知
earlyWarning:
  notice:
    # 一般预警通知
    generalDingNoticeUrl: xxx
    # 高频异常通知预警
    errorDingNoticeUrl: xxx

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

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

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

4.2.3 钉钉预警使用示例

  • 首先注入工具类

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

    /**
     * 钉钉预警通知测试,注意,如果你设定的机器人类型是关键字,内容需要包含关键字,才可以发送通知成功
     */
    @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()));
        }
    }
    

4.3 集成异常管理模块

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

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

4.3.1 添加异常模块依赖

    <dependency>
        <groupId>com.smilehappiness</groupId>
        <artifactId>smilehappiness-exception</artifactId>
    </dependency>

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());
        }
    }

4.4 集成限流管理模块

4.4.1 前言

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

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

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

4.4.2 添加限流管理模块依赖

     <dependency>
          <groupId>com.smilehappiness</groupId>
          <artifactId>smilehappiness-limit-api</artifactId>
     </dependency>

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 基于Spring Cloud 的微服务架构脚手架,满满的干货来啦~

基于Spring Cloud 的微服务架构脚手架,满满的干货来啦~

基于Spring Cloud的微服务架构分析

基于Spring Cloud的微服务架构分析

基于Spring Cloud的微服务架构分析

基于Spring Cloud的微服务落地