当前位置: 首页 > article >正文

【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简单⽅便很多, 但是还存在⼀些问题.

  1. 需要拼接URL, 灵活性⾼, 但是封装臃肿, URL复杂时, 容易出错.
  2. 代码可读性差, ⻛格不统⼀.

微服务之间的通信⽅式, 通常有两种: RPC 和 HTTP.

在SpringCloud中, 默认是使⽤HTTP来进⾏微服务的通信, 最常⽤的实现形式有两种:

  • RestTemplate
  • OpenFeign

RPC(Remote Procedure Call)远程过程调⽤,是⼀种通过⽹络从远程计算机上请求服务,⽽不需要了解底层⽹络通信细节。RPC可以使⽤多种⽹络协议进⾏通信, 如HTTP、TCP、UDP等, 并且在TCP/IP⽹络四层模型中跨越了传输层和应⽤层。简⾔之RPC就是像调⽤本地⽅法⼀样调⽤远程⽅法。

常⻅的RPC框架有:

  1. Dubbo: Apache Dubbo 中⽂
  2. Thrift : Apache Thrift - Home
  3. 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

调用流程

  1. 服务消费方 (order-service):通过TestFeignController类的o1方法发起HTTP请求。
  2. Feign客户端 (ProductApi)o1方法调用ProductApi接口的p1方法,将参数id传递给product-service
  3. 服务提供方 (product-service)ProductController类的p1方法接收请求,处理参数并返回结果。

调用流程解析

  1. 服务消费方:通过HTTP请求调用本地控制器(如TestFeignController),触发相应方法。
  2. Feign客户端:本地控制器方法调用Feign客户端接口方法,将参数传递给远程服务。
  3. 服务提供方:远程服务接收请求,处理传递的参数,执行相应逻辑并返回结果。

传递多个参数

使⽤多个 @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

服务消费方,引入抽取出来的模块

  1. 删除 ProductApi, ProductInfo
  2. 引⼊依赖
<dependency>
    <groupId>com.hsu</groupId>
    <artifactId>product-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

修改项⽬中ProductApi, ProductInfo的路径为product-api中的路径

  1. 指定扫描类: 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

服务部署

  1. 修改数据库, Nacos等相关配置

  2. 对两个服务进⾏打包

    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 改为本地仓库的路径

  1. 上传jar到Linux服务器

  2. 启动Nacos

    启动前最好把data数据删除掉.

  3. 启动服务

# 后台启动 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控制台

  1. 测试

访问接⼝: http://110.41.51.65:8080/order/1

观察远程调⽤的结果:


http://www.kler.cn/a/315032.html

相关文章:

  • 【C++学习(37)】并发性模式:如生产者-消费者、读写锁等。 架构模式:如MVC、MVVM等。属于23 种设计模式吗? RAII 的关系?
  • 卷积神经网络之Yolo详解
  • K8S单节点部署及集群部署
  • 【力扣热题100】[Java版] 刷题笔记-169. 多数元素
  • C++STL容器——map和set
  • SystemVerilog学习笔记(六):控制流
  • 鸿蒙【项目打包】- .hap 和 .app;(测试如何安装发的hap包)(应用上架流程)
  • 二二复制模式小程序商城开发
  • Python中的IPython:交互式的Python shell
  • 算法题之宝石与石头
  • 微服务、云计算、分布式开发全套课程课件,来原于企培和多年大厂工作提炼
  • el-form动态标题和输入值,并且最后一个输入框不校验
  • Python 课程16-OpenCV
  • C++门迷宫
  • C++高精度计时方法总结(测试函数运行时间)
  • Axios基本语法和前后端交互
  • 【数据结构】排序算法---计数排序
  • Cpp类和对象(中续)(5)
  • Rasa对话模型——做一个语言助手
  • Qt窗口——QToolBar
  • JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
  • python中ocr图片文字识别样例(二)
  • Spring MVC设置请求头和响应头的Header
  • Unity DOTS物理引擎的核心分析与详解
  • 植物大战僵尸【源代码分享+核心思路讲解】
  • [每日一练]MySQL中的正则表达式的应用