Spring Cloud笔记(4)构建Spring Cloud Demo

通过前几篇文章的积累,我们现在可以来动手搭建一个完整的Spring Cloud Demo项目了。为了更清楚的说明Spring Cloud的结构特点,我们的demo项目还是遵循由浅入深的原则,一开始只加入一些基本的特性,后面再来逐步完善。

业务背景

本来演示技术点的demo,弄一些sayHello的方法出来也无可厚非。但Spring Cloud的很多特性都是与业务的实际需求紧密结合的,脱离业务谈技术难免显得有些空洞,所以我们也需要为demo弄一个简单的业务背景。就用常见的订单管理和仓储管理来举例子吧,一个基本的业务流程就是客户创建订单 ,如下图:


客户下单流程.png

为此我们需要创建两个微服务模块:订单服务和仓储服务,订单服务提供创建订单的接口,创建订单的同时需要调用仓储服务提供的接口来对库存进行更新。

组件选择和环境搭建

之前我们介绍过,每一种Spring Cloud的特性的实现都有好几种不同的框架和工具进行选择,根据官方的推荐和技术的流行度,demo主要采用了以下的框架和工具:

  • Consul:用于服务的注册和发现
  • OpenFeign:声明式HTTP客户端,服务间调用
  • Spring Cloud Loadbalancer:客户端负载均衡
  • Hystrix:断路保护
  • Spring Cloud Gateway:智能路由,服务网关

这里面需要提前安装配置的组件只有一个Consul,可以参考我之前的文章 Consul的架构和配置

新建Maven项目

我们首先为所有的服务模块创建一个Parent项目,用于管理各种公共的依赖包,pom.xml如下,需要注意的地方请查看注释:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.github.davidfantasy</groupId>
    <artifactId>spring-cloud-demo</artifactId>
    <version>1.0.0</version>
    <modules>
        <!--订单管理服务-->
        <module>order-service</module>
        <!--仓储管理服务-->
        <module>storage-service</module>
    </modules>
    <name>spring-cloud-demo</name>
    <description>Demo project for Spring Cloud</description>
    <packaging>pom</packaging>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR3</spring-cloud.version>
        <spring-boot.version>2.2.6.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
            <!--如果不排除的话,默认会使用ribbon作为负载均衡器,会有警告日志-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <!--如果不排除的话,默认会使用ribbon作为负载均衡器,会有警告日志-->
            <exclusions>
                <exclusion>
                    <artifactId>spring-cloud-netflix-ribbon</artifactId>
                    <groupId>org.springframework.cloud</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--为服务提供默认的健康检查实现,否则需要自定义Consul的健康检查策略-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <!--管理spring-cloud的相关组件的版本号-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--管理spring-boot的相关组件的版本号-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

然后新建两个子模块:order-service和storage-service,子模块目前并不需要引入什么额外的依赖。接下来需要为每个模块创建一个启动类和添加一个系统配置文件,这部分两个模块其实都大同小异,以order-service为例:

服务启动类
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }

}
配置文件:application.yml
server:
  port: 9001
spring:
  application:
    name: order-service
  cloud:
    consul:
      host: 192.168.1.220
      port: 8500
      discovery:
        #使用IP地址而不是HOSTNAME作为服务的访问地址
        prefer-ip-address: true
feign:
  hystrix:
    #启用hystrix短路保护
    enabled: true
  okhttp:
    #feign使用okhttp来作为http客户端
    enabled: true

其实到这里,一个服务就已经基本搭建完成了,只需要填充具体的业务逻辑。这也体现了Spring Boot机制的强大之处,通过简单的配置加上各种starter,就能快速的将各种功能特性整合到项目中来。我们先来验证一下现在的配置,运行两个启动类,顺利的话,就能够在Consul的控制台http://localhost:8500/ui 看到服务成功注册的信息:

服务注册界面.png

服务注册的过程

service启动时,spring-cloud-starter-consul-discovery组件会根据配置信息生成一段服务注册的json报文,然后调用Consul Server的REST接口进行服务注册,比如order-service的注册信息是这样的(控制台有相应的输出信息):

{id='order-service-9001', name='order-service', tags=[secure=false], address='192.168.1.252', meta=null, port=9001, enableTagOverride=null, check=Check{script='null', interval='10s', ttl='null', http='http://192.168.1.252:9001/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null'}, checks=null}

从输出信息可以看出Consul对服务进行健康检查的回调地址是
http://192.168.1.252:9001/actuator/health,间隔时间是10s,如果需要修改默认的健康检查信息,可以通过设置相应的参数,请查看 这里。我们可以通过引入spring-boot-starter-actuator组件自动实现健康检查的回调,否则就需要自行定义了。

添加业务逻辑

首先在storage-service中添加一个API,用于模拟库存的变更,核心代码如下:

@RestController
@RequestMapping("/api/storage")
public class Controller {
    @Autowired
    private StorageService storageService;

    @PostMapping("/change-inventory")
    public Integer changeInventory(@RequestBody InventoryChangeDTO req) {
        return storageService.changeInventory(req);
    }
}

storageService.changeInventory方法模拟对库存的扣减操作,并返回一个Integer表示当前剩余的库存数(默认返回98)。然后在order-service中添加一个Feign Client接口,用于声明需要远程调用的storage-service的相关接口:

@FeignClient(name = "storage-service", fallback = StorageServiceFallback.class)
public interface StorageService {

    @PostMapping("/api/storage/change-inventory")
    Integer updateInventoryOfGood(InventoryChangeDTO inventoryChangeDTO);

}

这个接口类相当于是order-service访问storage-service中API的一个代理,@FeignClient的name字段中指定的名称需要与storage-service注册的服务名称保持一致,这样feign会通过服务名查询Consul中已注册的服务,并自动获取order-service的访问地址。如果order-service部署了多个实例,Feign会使用Spring Cloud Loadbalancer进行相应的负载均衡(这个不需要额外的配置,只需要在依赖包中引入相应的starter即可)。fallback = StorageServiceFallback.class 声明了如果storage-service中相应的接口不可用的时候,需要进行相应的降级处理,这个是利用到了Hystrix的熔断保护特性,需要在配置文件中声明:

feign.hystrix.enabled=true

然后我们来构造order-service的createOrder接口,用于用户创建订单:

@RestController
@RequestMapping("/api/order")
@Slf4j
public class Controller {

    @Autowired
    private OrderService orderService;

    @Autowired
    private StorageService storageService;

    @PostMapping("/create-order")
    public String createOrder(@RequestBody OrderDTO order) {
        //创建新订单
        orderService.createNewOrder(order);
        InventoryChangeDTO req = new InventoryChangeDTO();
        req.setGoodCode(order.getGoodCode());
        req.setQuantity(order.getQuantity());
        //调用仓储服务变更库存
        Integer remainQuantity = storageService.updateInventoryOfGood(req);
        log.info("剩余数量:" + remainQuantity);
        return "ok";
    }

}

这样基本的业务流程已经完成了,我们来编写一个单元测试来测试一下订单创建的接口:

    @Test
    public void testCreateOrder() throws Exception {
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setCustomerCode("cus001");
        orderDTO.setGoodCode("tc-1");
        orderDTO.setQuantity(100);
        this.mvc.perform(post("/api/order/create-order").content(JsonUtil.obj2json(orderDTO))
                .contentType("application/json"))
                .andExpect(status().isOk())
                .andExpect(content().string("ok"));
    }

运行之后,一切正常的话,就可以在控制台看到输出的剩余库存日志信息:

2020-04-16 10:03:58.846  INFO 8276 --- [           main] c.g.d.s.o.controller.Controller          : 剩余数量:98

熔断降级

如果当远程服务不可用的时候,需要做一些额外的处理,比如加入重试队列后期进行重试,记录错误日志等等,就需要用到Hystrix提供的熔断保护特性了。之前说到的StorageServiceFallback就是用于这个目的的,我们来看一下StorageServiceFallback的代码:

@Component
@Slf4j
public class StorageServiceFallback implements StorageService {

    public Integer updateInventoryOfGood(InventoryChangeDTO inventoryChangeDTO) {
        log.error("StorageServiceFallback.updateInventoryOfGood暂不可用");
        return -1;
    }

}

这里的业务处理很简单,只是返回一个默认数字-1,并打印了一行错误日志。现在先把StorageServiceApplication停止,然后再执行testCreateOrder方法,就会看到如下的输出日志:

2020-04-16 10:55:09.245 ERROR 6432 --- [ HystrixTimer-1] c.g.d.s.o.remote.StorageServiceFallback  : StorageServiceFallback.updateInventoryOfGood暂不可用
2020-04-16 10:55:09.245  INFO 6432 --- [           main] c.g.d.s.o.controller.Controller          : 剩余数量:-1

从日志上就可以看出降级服务已经生效了。熔断降级在一些中小型的系统中可能意义不太大,但还是可以利用这个机制来做一些其它的应用,比如对单个服务进行单元测试时,其依赖的远程服务都需要打桩,通过降级机制我们就可以很容易的生成远程服务的MOCK接口,定制自己的测试逻辑了。

本文的相关代码可以查看这里 spring-cloud-demo

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容