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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值