通过前几篇文章的积累,我们现在可以来动手搭建一个完整的Spring Cloud Demo项目了。为了更清楚的说明Spring Cloud的结构特点,我们的demo项目还是遵循由浅入深的原则,一开始只加入一些基本的特性,后面再来逐步完善。
业务背景
本来演示技术点的demo,弄一些sayHello的方法出来也无可厚非。但Spring Cloud的很多特性都是与业务的实际需求紧密结合的,脱离业务谈技术难免显得有些空洞,所以我们也需要为demo弄一个简单的业务背景。就用常见的订单管理和仓储管理来举例子吧,一个基本的业务流程就是客户创建订单 ,如下图:
为此我们需要创建两个微服务模块:订单服务和仓储服务,订单服务提供创建订单的接口,创建订单的同时需要调用仓储服务提供的接口来对库存进行更新。
组件选择和环境搭建
之前我们介绍过,每一种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 看到服务成功注册的信息:
服务注册的过程
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