通过前几篇文章的积累,我们现在可以来动手搭建一个完整的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如下,需要注意的地方请查看注释:
4.0.0
com.github.davidfantasy
spring-cloud-demo
1.0.0
order-service
storage-service
spring-cloud-demo
Demo project for Spring Cloud
pom
1.8
Hoxton.SR3
2.2.6.RELEASE
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-consul-discovery
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
org.springframework.cloud
spring-cloud-starter-loadbalancer
org.springframework.cloud
spring-cloud-starter-openfeign
spring-cloud-netflix-ribbon
org.springframework.cloud
org.springframework.boot
spring-boot-starter-actuator
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
然后新建两个子模块: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