【SpringCloud】优雅实现远程调用 - OpenFeign
目录
- 优雅实现远程调用-OpenFeign
- RestTemplate存在问题
- OpenFeign介绍
- Spring Cloud Feign
- 快速上手
- 引入依赖
- 添加注解
- 编写OpenFeign的客户端
- 远程调用
- 测试
- OpenFeign参数传递
- 传递单个参数
- 传递多个参数
- 传递对象
- 传递JSON
- 最佳实践
- Feign 继承方式
- 创建⼀个Module
- 引入依赖
- 编写接口
- 打Jar包
- 服务提供方
- 服务消费方
- 测试
- Feign 抽取方式
- 创建⼀个module
- 引入依赖
- 编写API
- 打Jar包
- 服务消费方使用product-api
- 测试
- 服务部署
优雅实现远程调用-OpenFeign
RestTemplate存在问题
观察我们远程调⽤的代码
public OrderInfo selectOrderById(Integer orderId) {
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
String url = "http://product-service/product/" + orderInfo.getProductId();
ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
虽说RestTemplate 对HTTP封装后, 已经⽐直接使⽤HTTPClient简单⽅便很多, 但是还存在⼀些问题.
- 需要拼接URL, 灵活性⾼, 但是封装臃肿, URL复杂时, 容易出错.
- 代码可读性差, ⻛格不统⼀.
微服务之间的通信⽅式, 通常有两种: RPC 和 HTTP.
在SpringCloud中, 默认是使⽤HTTP来进⾏微服务的通信, 最常⽤的实现形式有两种:
- RestTemplate
- OpenFeign
RPC(Remote Procedure Call)远程过程调⽤,是⼀种通过⽹络从远程计算机上请求服务,⽽不需要了解底层⽹络通信细节。RPC可以使⽤多种⽹络协议进⾏通信, 如HTTP、TCP、UDP等, 并且在TCP/IP⽹络四层模型中跨越了传输层和应⽤层。简⾔之RPC就是像调⽤本地⽅法⼀样调⽤远程⽅法。
常⻅的RPC框架有:
- Dubbo: Apache Dubbo 中⽂
- Thrift : Apache Thrift - Home
- gRPC: gRPC
OpenFeign介绍
OpenFeign 是⼀个声明式的 Web Service 客⼾端. 它让微服务之间的调⽤变得更简单, 类似controller调⽤service, 只需要创建⼀个接⼝,然后添加注解即可使⽤OpenFeign.
OpenFeign 的前⾝
Feign 是 Netflix 公司开源的⼀个组件.
-
2013年6⽉, Netflix发布 Feign的第⼀个版本 1.0.0
-
2016年7⽉, Netflix发布Feign的最后⼀个版本 8.18.0
2016年,Netflix 将 Feign 捐献给社区
-
2016年7⽉ OpenFeign 的⾸个版本 9.0.0 发布,之后⼀直持续发布到现在.
可以简单理解为 Netflix Feign 是OpenFeign的祖先, 或者说OpenFeign 是Netflix Feign的升级版.
OpenFeign 是Feign的⼀个更强⼤更灵活的实现.
我们现在⽹络上看到的⽂章, 或者公司使⽤的Feign, ⼤多都是OpenFeign.
后续讲的Feign, 指的是OpenFeign
Spring Cloud Feign
Spring Cloud Feign 是 Spring 对 Feign 的封装, 将 Feign 项⽬集成到 Spring Cloud ⽣态系统中.
受 Feign 更名影响,Spring Cloud Feign 也有两个 starter
spring-cloud-starter-feign
spring-cloud-starter-openfeign
由于Feign的停更维护, 对应的, 我们使⽤的依赖是 spring-cloud-starter-openfeign
OpenFeign 官⽅⽂档: GitHub - OpenFeign/feign: Feign makes writing java http clients easier
Spring Cloud Feign⽂档: Spring Cloud OpenFeign
快速上手
因为 Feign 的学习是基于 Nacos 的代码进行开发的,因此复制 spring-cloud-nacos 项目为 spring-cloud-feign,记得修改对应的 pom.xml 文件
引入依赖
在 order-service 中
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
添加注解
在order-service的启动类添加注解 @EnableFeignClients
, 开启OpenFeign的功能.
@EnableFeignClients
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
编写OpenFeign的客户端
基于SpringMVC的注解来声明远程调⽤的信息
在 api 包下
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {
@RequestMapping("/{productId}")
ProductInfo getProductById(@PathVariable("productId") Integer productId);
}
@FeignClient
注解作⽤在接⼝上, 参数说明:
- name/value:指定FeignClient的名称, 也就是微服务的名称, ⽤于服务发现, Feign底层会使⽤Spring Cloud LoadBalance进⾏负载均衡. 也可以使⽤ url 属性指定⼀个具体的url.
- path: 定义当前FeignClient的统⼀前缀.
远程调用
修改远程调⽤的⽅法
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ProductApi productApi;
/**
* Feign实现远程调用
*/
public OrderInfo selectOrderById(Integer orderId) {
OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
ProductInfo productInfo = productApi.getProductById(orderInfo.getProductId());
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
}
测试
启动服务, 访问接⼝, 测试远程调⽤:
http://127.0.0.1:8080/order/1
可以看出来, 使⽤Feign也可以实现远程调⽤.
Feign 简化了与HTTP服务交互的过程, 把REST客⼾端的定义转换为Java接⼝, 并通过注解的⽅式来声明请求参数,请求⽅式等信息, 使远程调⽤更加⽅便和间接.
OpenFeign参数传递
通过观察, 我们也可以发现, Feign的客⼾端和服务提供者的接⼝声明⾮常相似
上⾯例⼦中, 演⽰了Feign 从URL中获取参数, 接下来演⽰下Feign参数传递的其他⽅式
只做代码演⽰, 不做功能
传递单个参数
服务提供⽅ product-service
@RequestMapping("/product")
@RestController
public class ProductController {
@RequestMapping("/p1")
public String p1(Integer id){
return "p1接收到参数:" + id;
}
}
Feign客⼾端
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {
@RequestMapping("/p1")
String p1(@RequestParam("id") Integer id);
}
注意:
@RequestParam
做参数绑定,会将请求中的id
的参数值绑定到方法的id
变量,不能省略
服务消费⽅ order-service
@RequestMapping("/feign")
@RestController
public class TestFeignController {
@Autowired
private ProductApi productApi;
@RequestMapping("/o1")
public String o1(Integer id){
return productApi.p1(id);
}
}
测试远程调⽤
http://127.0.0.1:8080/feign/o1?id=5
调用流程
- 服务消费方 (
order-service
):通过TestFeignController
类的o1
方法发起HTTP请求。 - Feign客户端 (
ProductApi
):o1
方法调用ProductApi
接口的p1
方法,将参数id
传递给product-service
。 - 服务提供方 (
product-service
):ProductController
类的p1
方法接收请求,处理参数并返回结果。
调用流程解析
- 服务消费方:通过HTTP请求调用本地控制器(如
TestFeignController
),触发相应方法。 - Feign客户端:本地控制器方法调用Feign客户端接口方法,将参数传递给远程服务。
- 服务提供方:远程服务接收请求,处理传递的参数,执行相应逻辑并返回结果。
传递多个参数
使⽤多个 @RequestParam
进⾏参数绑定即可
服务提供⽅ product-service
@RequestMapping("/product")
@RestController
public class ProductController {
@RequestMapping("/p2")
public String p2(Integer id, String name){
return "p2接收到参数,id:" + id + ",name:" + name;
}
}
Feign客⼾端
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {
@RequestMapping("/p2")
String p2(@RequestParam("id") Integer id, @RequestParam("name") String name);
}
服务消费⽅ order-service
@RequestMapping("/feign")
@RestController
public class TestFeignController {
@Autowired
private ProductApi productApi;
@RequestMapping("/o2")
public String o2(@RequestParam("id") Integer id, @RequestParam("name") String name){
return productApi.p2(id, name);
}
}
测试远程调⽤
http://127.0.0.1:8080/feign/o2?id=5&name=zhangsan
传递对象
服务提供⽅ product-service
@RequestMapping("/product")
@RestController
public class ProductController {
@RequestMapping("/p3")
public String p3(ProductInfo productInfo){
return "接收到对象, productInfo:" + productInfo;
}
}
Feign客⼾端
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {
@RequestMapping("/p3")
String p3(@SpringQueryMap ProductInfo productInfo);
}
@SpringQueryMap
注解将ProductInfo
对象的属性作为查询参数传递给远程服务
服务消费⽅ order-service
@RequestMapping("/feign")
@RestController
public class TestFeignController {
@Autowired
private ProductApi productApi;
@RequestMapping("/o3")
public String o3(ProductInfo productInfo){
return productApi.p3(productInfo);
}
}
测试远程调⽤
http://127.0.0.1:8080/feign/o3?id=5&productName=zhangsan
传递JSON
服务提供⽅ product-service
@RequestMapping("/product")
@RestController
public class ProductController {
@RequestMapping("/p4")
public String p4(@RequestBody ProductInfo productInfo){
return "接收到对象, productInfo:" + productInfo;
}
}
@RequestBody
: 用于将 HTTP 请求体绑定到方法参数productInfo
上。Spring MVC 会自动将 JSON 数据反序列化为ProductInfo
对象
Feign客⼾端
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {
@RequestMapping("/p4")
String p4(@RequestBody ProductInfo productInfo);
}
@RequestBody
: 用于标注客户端方法参数productInfo
。Feign 会自动将ProductInfo
对象序列化为 JSON 数据,并在请求体中传递
服务消费⽅ order-service
@RequestMapping("/feign")
@RestController
public class TestFeignController {
@Autowired
private ProductApi productApi;
@RequestMapping("/o4")
public String o4(@RequestBody ProductInfo productInfo){
System.out.println(productInfo.toString());
return productApi.p4(productInfo);
}
}
@RequestBody
: 用于将 HTTP 请求体绑定到方法参数productInfo
上。Spring MVC 会自动将 JSON 数据反序列化为ProductInfo
对象
测试远程调⽤
http://127.0.0.1:8080/feign/o4
最佳实践
最佳实践, 其实也就是经过历史的迭代, 在项⽬中的实践过程中, 总结出来的最好的使⽤⽅式.
通过观察, 我们也能看出来, Feign的客⼾端与服务提供者的controller代码⾮常相似
Feign 客⼾端
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {
@RequestMapping("/{productId}")
ProductInfo getProductById(@PathVariable("productId") Integer productId);
}
服务提供⽅Controller
@RequestMapping("/product")
@RestController
public class ProductController {
@RequestMapping("/{productId}")
public ProductInfo getProductById(@PathVariable("productId") Integer productId) {
// Implementation details
}
}
有没有⼀种⽅法可以简化这种写法呢?
Feign 继承方式
Feign ⽀持继承的⽅式, 我们可以把⼀些常⻅的操作封装到接⼝⾥.
我们可以定义好⼀个接⼝, 服务提供⽅实现这个接⼝, 服务消费⽅编写Feign 接⼝的时候, 直接继承这个接⼝
具体参考: Spring Cloud OpenFeign Features :: Spring Cloud Openfeign
注意:我们需要复制一份 spring-cloud-feign 项目,因为我们后续别的学习还是要用到这个项目的代码的,我们复制之后保留备份即可,依然在同一个项目中写下面的代码
创建⼀个Module
接⼝可以放在⼀个公共的Jar包⾥, 供服务提供⽅和服务消费⽅使⽤
引入依赖
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign Starter -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
编写接口
复制 ProductApi, ProductInfo 到product-api模块中
ProductInterface 写在 api 包下,ProductInfo 复制到 model 包下,然后 order-service 和 product-service 服务里的 ProductInfo 就可以注释掉了,因为这些是已经提取到公共的 product-api 模块里了
public interface ProductInterface {
@RequestMapping("/{productId}")
ProductInfo getProductById(@PathVariable("productId") Integer productId);
@RequestMapping("/p1")
String p1(@RequestParam("id") Integer id);
@RequestMapping("/p2")
String p2(@RequestParam("id") Integer id, @RequestParam("name") String name);
@RequestMapping("/p3")
String p3(@SpringQueryMap ProductInfo productInfo);
@RequestMapping("/p4")
String p4(@RequestBody ProductInfo productInfo);
}
⽬录结构如下:
打Jar包
通过Maven把当前工程打成jar包,放在Maven本地仓库(不是远程仓库)
观察Maven本地仓库, Jar包是否打成功
[INFO] Installing D:\Git\spring-cloud\spring-cloud-feign2\product-api\target\product-api-1.0-SNAPSHOT.jar to
D:\Maven\.m2\repository\org\example\product-api\1.0-SNAPSHOT\product-api-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.796 s
[INFO] Finished at: 2024-01-03T19:31:35+08:00
[INFO] ------------------------------------------------------------------------
这里打包到的地址是你要在 IDEA 里设置 Maven 的本地仓库的地址,然后加上的是包的地址,我这里是
com\hsu\product-api
,然后就可以注释掉原来 order-service 和 product-service 里的 ProductInfo 类了直接在 order-service 和 product-service 里导入下面的即可,但可能还需要修改一下对应其他包里引用的 ProductInfo,有可能还是之前的
<dependency> <groupId>com.hsu</groupId> <artifactId>product-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
服务提供方
服务提供⽅实现接⼝ ProductInterface
@Slf4j
@RequestMapping("/product")
@RestController
public class ProductController implements ProductInterface {
@Autowired
private ProductService productService;
@RequestMapping("/{productId}")
public ProductInfo getProductById(@PathVariable("productId") Integer productId) {
log.info("接收参数, productId:{}", productId);
return productService.selectProductById(productId);
}
@RequestMapping("/p1")
public String p1(Integer id) {
return "p1接收到参数: " + id;
}
@RequestMapping("/p2")
public String p2(Integer id, String name) {
return "p2接收到参数, id: " + id + ", name: " + name;
}
@RequestMapping("/p3")
public String p3(ProductInfo productInfo) {
return "接收到对象, productInfo: " + productInfo;
}
@RequestMapping("/p4")
public String p4(@RequestBody ProductInfo productInfo) {
return "接收到对象, productInfo: " + productInfo;
}
}
服务消费方
服务消费⽅继承ProductInterface
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi extends ProductInterface {
}
测试
试远程调⽤
http://127.0.0.1:8080/order/1
Feign 抽取方式
官⽅推荐Feign的使⽤⽅式为继承的⽅式, 但是企业开发中, 更多是把Feign接⼝抽取为⼀个独⽴的模块(做法和继承相似, 但理念不同).
先将原来备份的 spring-cloud-feign 改名为 spring-cloud-feign2,我们在这里学习
操作⽅法:
将Feign的Client抽取为⼀个独⽴的模块, 并把涉及到的实体类等都放在这个模块中, 打成⼀个Jar. 服务消费⽅只需要依赖该Jar包即可. 这种⽅式在企业中⽐较常⻅, Jar包通常由服务提供⽅来实现.
创建⼀个module
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
编写API
复制 ProductApi, ProductInfo 到product-api模块中
这里目录格式有点问题,api 和 model 是同级的
打Jar包
通过Maven打包product-api
观察Maven本地仓库, Jar包是否打成功
[INFO] Installing D:\Git\spring-cloud\spring-cloud-feign\product-api\target\product-api-1.0-SNAPSHOT.jar to
D:\Maven\.m2\repository\org\example\product-api\1.0-SNAPSHOT\product-api-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.441 s
[INFO] Finished at: 2024-01-03T17:55:14+08:00
[INFO] ------------------------------------------------------------------------
服务消费方使用product-api
服务消费方,引入抽取出来的模块
- 删除 ProductApi, ProductInfo
- 引⼊依赖
<dependency>
<groupId>com.hsu</groupId>
<artifactId>product-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
修改项⽬中ProductApi, ProductInfo的路径为product-api中的路径
- 指定扫描类: ProductApi
在启动类添加扫描路径
@EnableFeignClients(basePackages = {"com.hsu.product.api"})
完整代码如下:
@EnableFeignClients(basePackages = {"com.hsu.product.api"})
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
也可以指定需要加载的Feign客⼾端
@EnableFeignClients(clients = {ProductApi.class})
测试
测试远程调⽤
http://127.0.0.1:8080/order/1
服务部署
-
修改数据库, Nacos等相关配置
-
对两个服务进⾏打包
Maven打包默认是从远程仓库下载的, product-api 这个包在本地, 有以下解决⽅案:
-
上传到Maven中央仓库(参考: 如何发布Jar包到Maven中央仓库, ⽐较⿇烦)[不推荐]
-
搭建Maven私服, 上传Jar包到私服[企业推荐]
-
从本地读取Jar包[个⼈学习阶段推荐]
前两种⽅法⽐较复杂, 我们使⽤第三种⽅式
修改pom⽂件
-
<dependency>
<groupId>org.example</groupId>
<artifactId>product-api</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- scope 为 system. 此时必须提供 systemPath 即本地依赖路径。表示 Maven 不会去中央仓库查找依赖,不推荐使用 -->
<scope>system</scope>
<systemPath>D:/Maven/.m2/repository/org/example/product-api/1.0-SNAPSHOT/product-api-1.0-SNAPSHOT.jar</systemPath>
</dependency>
<!-- .... -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>
把D:/Maven/.m2/repository 改为本地仓库的路径
-
上传jar到Linux服务器
-
启动Nacos
启动前最好把data数据删除掉.
-
启动服务
# 后台启动 order-service,并设置输出日志到 logs/order.log
nohup java -jar order-service.jar > logs/order.log &
# 后台启动 product-service,并设置输出日志到 logs/product-9090.log
nohup java -jar product-service.jar > logs/product-9090.log &
# 启动实例,指定端口号为 9091,并设置输出日志到 logs/product-9091.log
nohup java -jar product-service.jar --server.port=9091 > logs/product-9091.log &
观察Nacos控制台
- 测试
访问接⼝: http://110.41.51.65:8080/order/1
观察远程调⽤的结果: