前面我们讲了一下 Ribbon 和 RestTemplate 实现服务端通信的方法,Ribbon 提供了客户端负载均衡,而 RestTemplate 则对 http 进行封装,简化了发送请求的流程,两者互相配合,构建了服务间的高可用通信。
但在使用后也会发现,RestTemplate 只是对 HTTP 做了简单的封装,像发送请求的 URL、参数、请求头、请求体这些细节都需要我们自己处理,如此底层的操作都暴露出来肯定是不利于团队间协作的,因此就需要一种封装度更高,使用更简单的技术来屏蔽通信底层的复杂度,这里就来到了我们这篇文章介绍的重点了:OpenFeign 技术。
为了便于理解,我们这里通过一个具体的案例来配合理解。
一、案例背景
在某电商平台的订单业务中,为了保证商品不超卖,我们需要在下单时查询商品库存,如有库存则创建订单,继续支付流程,如果库存为 0,则提示用户库存不足,无法下单。这里我们来定义订单服务(order-service)和仓储服务(warehouse-service)。总体流程如下:
在上述业务中,订单服务是依赖仓储服务的,那仓储服务就是服务提供者,订单服务就是服务消费者,梳理清思路后,我们来使用代码还原这个场景。
二、创建服务提供者(warehouse-service)
仓储服务做为服务提供者,就是标准的 springboot 工程,我们先创建一个 springboot 工程。
1、工程创建
利用 Spring Initializr 向导创建 warehouse-service 工程(前面文章有创建步骤,不明白的可以去看一下)。确保在创建后的 pom.xml 中有如下引用:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2、配置注册中心
在创建好的工程中的 application.yml 文件中新增 Nacos 通信配置。
spring:
application:
name: warehouse-service #应用/微服务名字
cloud:
nacos:
discovery:
server-addr: 106.14.221.171:8848 #nacos服务器地址
username: nacos #用户名密码
password: nacos
server:
port: 80
3、创建库存实体类
创建库存实体类,保存库存信息。
package com.example.warehouseservice.dto;
//库存商品对象
public class Stock {
private Long skuId; //商品品类编号
private String title; //商品与品类名称
private Integer quantity; //库存数量
private String unit; //单位
private String description; //描述信息
//带参构造函数
public Stock(Long skuId, String title, Integer quantity, String unit) {
this.skuId = skuId;
this.title = title;
this.quantity = quantity;
this.unit = unit;
}
//getter and setter省略...
}
4、创建控制器(controller)
创建仓储服务控制器 WarehouseController,通过一个 getStock()方法传入商品编号,返回具体的库存数据。我们这里采用数据模拟的方式,定义两个商品库存:编号为1101 的是紫色 256G iPhone15,库存 32 台,编号1102 的是白色 256G iPhone15,库存为 0。
package com.example.warehouseservice.controller;
//省略 import 部分
//仓储服务控制器
@RestController
public class WarehouseController {
/**
* 查询对应 skuId 的库存状况
* @param skuId skuId
* @return Stock 库存对象
*/
@GetMapping("/stock")
public Stock getStock(Long skuId){
Map result = new HashMap();
Stock stock = null;
if(skuId == 1101l){
//模拟有库存商品
stock = new Stock(1101l, "Apple iPhone 15 128GB 紫色", 32, "台");
stock.setDescription("Apple 11 紫色版对应商品描述");
}else if(skuId == 1102l){
//模拟无库存商品
stock = new Stock(1101l, "Apple iPhone 15 256GB 白色", 0, "台");
stock.setDescription("Apple 11 白色版对应商品描述");
}else{
//演示案例,暂不考虑无对应 skuId 的情况
}
return stock;
}
}
5、服务启动
上述代码完成后,我们打包部署到服务器上,启动成功后,可以在 Nacos 注册中心中看到注册状态
再在浏览器中访问 url 来查看服务返回的数据:
http://192.168.3.2/stock?skuId=1101
{
skuId: 1101,
title: "Apple iPhone 15 128GB 紫色",
quantity : 32,
unit: "台",
description:"Apple 11 紫色版对应商品描述"
}
至此,我们服务提供者 warehouse-service 就开发完成了,下面我们来开发服务消费者。
三、创建服务消费者(order-service)
1、工程创建
我们还是使用 Spring initializr 创建一个 order-service 工程,并确保 pom.xml 中引入如下包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
2、启用 OpenFeign
创建完并添加好工程依赖包后,我们需要在应用入口 OrderServiceApplication 中添加@EnableFeignClients 注解,这里是为了通知 Spring 启用 OpenFeign 声明式通信。
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients //启用OpenFeign
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
3、配置 Nacos
默认的 OpenFeign 并不需要任何的配置,我们在 application.yml 配置一下 Nacos。
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: 106.14.221.171:8848
username: nacos
password: nacos
server:
port: 80
4、创建 OpenFeign 通信接口和响应对象
package com.example.orderservice.feignclient;
import com.example.orderservice.dto.Stock;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient("warehouse-service")
public interface WarehouseServiceFeignClient {
@GetMapping("/stock")
public Stock getStock(@RequestParam("skuId") Long skuId);
}
在 order-service 工程下,创建一个 feignclient 包用于保存通信接口。OpenFeign 通过“接口+注解”形式描述数据传输逻辑,并不需要我们编写具体实现代码便能实现服务间高可用通信。
@FeignClient 注解说明当前接口为 OpenFeign 通信客户端,参数值 warehouse-service 为服务提供者 ID,这一项必须与 Nacos 注册 ID 保持一致。在 OpenFeign 发送请求前会自动在 Nacos 查询 warehouse-service 所有可用实例信息,再通过内置的 Ribbon 负载均衡选择一个实例发起 RESTful 请求,进而保证通信高可用.
package com.lagou.orderservice.dto;
//消费者端接收响应Stock对象
public class Stock {
private Long skuId; //商品品类编号
private String title; //商品与品类名称
private Integer quantity; //库存数量
private String unit; //单位
@Override
public String toString() {
return "Stock{" +
"skuId=" + skuId +
", title='" + title + ''' +
", quantity=" + quantity +
", unit='" + unit + ''' +
'}';
}
//getter与setter省略
}
声明的方法结构,接口中定义的方法通常与服务提供者的方法定义保持一致。这里有个非常重要的细节:用于接收数据的 Stock 对象并不强制要求与提供者端 Stock 对象完全相同,消费者端的 Stock 类可以根据业务需要删减属性,但属性必须要与提供者响应的 JSON 属性保持一致。距离说明,我们在代码发现消费者端 Stock 的包名与代码与提供者都不尽相同,而且因为消费者不需要 description 属性便将其删除,其余属性只要保证与服务提供者响应 JSON 保持一致,在 OpenFeign 获取响应后便根据 JSON 属性名自动反序列化到 Stock 对象中。
5、接口注入,远程调用
在消费者 Controller 中对 FeignClient 接口进行注入,像调用本地方法一样调用即可。
package com.example.orderservice.controller;
import com.example.orderservice.dto.Stock;
import com.example.orderservice.feignclient.WarehouseServiceFeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
public class OrderController {
//利用@Resource将IOC容器中自动实例化的实现类对象进行注入
@Resource
private WarehouseServiceFeignClient warehouseServiceFeignClient;
/**
* 创建订单业务逻辑
* @param skuId 商品类别编号
* @param salesQuantity 销售数量
* @return
*/
@GetMapping("/create_order")
public Map createOrder(Long skuId , Long salesQuantity){
Map result = new LinkedHashMap();
//查询商品库存,像调用本地方法一样完成业务逻辑。
Stock stock = warehouseServiceFeignClient.getStock(skuId);
System.out.println(stock);
if(salesQuantity <= stock.getQuantity()){
//创建订单相关代码,此处省略
//CODE=SUCCESS代表订单创建成功
result.put("code" , "SUCCESS");
result.put("skuId", skuId);
result.put("message", "订单创建成功");
}else{
//code=NOT_ENOUGN_STOCK代表库存不足
result.put("code", "NOT_ENOUGH_STOCK");
result.put("skuId", skuId);
result.put("message", "商品库存数量不足");
}
return result;
}
}
6、部署测试
将消费者部署后,我们尝试调用消费者的创建订单接口,如传入 1101 编号,则会出现以下返回:
http://192.168.3.3/create_order?skuId=1101&salesQuantity=1
{
code: "SUCCESS",
skuId: 1101,
message: "订单创建成功"
}
如传入 1102 编号,则会出现以下返回:
http://192.168.3.3/create_order?skuId=1102&salesQuantity=1
{
code: "NOT_ENOUGH_STOCK",
skuId: 1102,
message: "商品库存数量不足"
}
这里已经基于 OpenFeign 实现了服务间通信。
到这里,我们 SpringCloud 集成 OpenFeign 的工作就完成了,大家可以按照自己的业务愉快的撸码了。