【SpringCloud】优雅实现远程调用 - OpenFeign

目录


优雅实现远程调用-OpenFeign

RestTemplate存在问题

观察我们远程调⽤的代码

java 复制代码
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 中

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

添加注解

在order-service的启动类添加注解 @EnableFeignClients , 开启OpenFeign的功能.

java 复制代码
@EnableFeignClients
@SpringBootApplication
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

编写OpenFeign的客户端

基于SpringMVC的注解来声明远程调⽤的信息

在 api 包下

java 复制代码
@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的统⼀前缀.

远程调用

修改远程调⽤的⽅法

java 复制代码
@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

java 复制代码
@RequestMapping("/product")
@RestController
public class ProductController {

    @RequestMapping("/p1")
    public String p1(Integer id){
        return "p1接收到参数:" + id;
    }
}

Feign客⼾端

java 复制代码
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {

    @RequestMapping("/p1")
    String p1(@RequestParam("id") Integer id);
}

注意: @RequestParam 做参数绑定,会将请求中的 id 的参数值绑定到方法的 id 变量,不能省略

服务消费⽅ order-service

java 复制代码
@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

java 复制代码
@RequestMapping("/product")
@RestController
public class ProductController {

    @RequestMapping("/p2")
    public String p2(Integer id, String name){
        return "p2接收到参数,id:" + id + ",name:" + name;
    }
}

Feign客⼾端

java 复制代码
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {

    @RequestMapping("/p2")
    String p2(@RequestParam("id") Integer id, @RequestParam("name") String name);
}

服务消费⽅ order-service

java 复制代码
@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

java 复制代码
@RequestMapping("/product")
@RestController
public class ProductController {

    @RequestMapping("/p3")
    public String p3(ProductInfo productInfo){
        return "接收到对象, productInfo:" + productInfo;
    }
}

Feign客⼾端

java 复制代码
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {

    @RequestMapping("/p3")
    String p3(@SpringQueryMap ProductInfo productInfo);
}

@SpringQueryMap 注解将 ProductInfo 对象的属性作为查询参数传递给远程服务

服务消费⽅ order-service

java 复制代码
@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

java 复制代码
@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客⼾端

java 复制代码
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {

    @RequestMapping("/p4")
    String p4(@RequestBody ProductInfo productInfo);
}

@RequestBody: 用于标注客户端方法参数 productInfo。Feign 会自动将 ProductInfo 对象序列化为 JSON 数据,并在请求体中传递

服务消费⽅ order-service

java 复制代码
@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 客⼾端

java 复制代码
@FeignClient(value = "product-service", path = "/product")
public interface ProductApi {

    @RequestMapping("/{productId}")
    ProductInfo getProductById(@PathVariable("productId") Integer productId);
}

服务提供⽅Controller

java 复制代码
@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包⾥, 供服务提供⽅和服务消费⽅使⽤

引入依赖
xml 复制代码
<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 模块里了

java 复制代码
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包是否打成功

bash 复制代码
[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,有可能还是之前的

xml 复制代码
<dependency>
 <groupId>com.hsu</groupId>
 <artifactId>product-api</artifactId>
 <version>1.0-SNAPSHOT</version>
</dependency>
服务提供方

服务提供⽅实现接⼝ ProductInterface

java 复制代码
@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

java 复制代码
@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
引入依赖
xml 复制代码
<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包是否打成功

bash 复制代码
[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. 引⼊依赖
xml 复制代码
<dependency>
    <groupId>com.hsu</groupId>
    <artifactId>product-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

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

  1. 指定扫描类: ProductApi

在启动类添加扫描路径

java 复制代码
@EnableFeignClients(basePackages = {"com.hsu.product.api"})

完整代码如下:

java 复制代码
@EnableFeignClients(basePackages = {"com.hsu.product.api"})
@SpringBootApplication
public class OrderServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

也可以指定需要加载的Feign客⼾端

java 复制代码
@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⽂件

xml 复制代码
<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. 启动服务

bash 复制代码
# 后台启动 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

观察远程调⽤的结果:

相关推荐
布说在见11 分钟前
Spring Boot管理用户数据
java·spring boot·后端
职教育人19 分钟前
Docker UI强大之处?
spring cloud·docker·容器
coder what30 分钟前
基于springboot的图书管理系统
java·spring boot·后端·图书管理系统
码农小旋风30 分钟前
一文详解大语言模型Transformer结构
后端
熙客1 小时前
Spring MVC的应用
java·spring·mvc
A乐神1 小时前
Django 基础之启动命令和基础配置
后端·python·django
coffee_baby2 小时前
策略模式在 Spring Boot 框架中的应用
java·spring boot·后端·策略模式
Flying_Fish_roe2 小时前
Spring Boot文件上传/下载问题
java·spring boot·后端
haozihua2 小时前
Spring6梳理10—— 依赖注入之注入数组类型属性
java·spring
ζั͡ޓއއއ坏尐絯2 小时前
ByteCinema(1):用户的登录注册
java·后端·开发记录