【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

观察远程调⽤的结果:

相关推荐
2401_8543910810 分钟前
城镇住房保障:SpringBoot系统功能概览
java·spring boot·后端
陈随易15 分钟前
兔小巢收费引发的论坛调研Node和Deno有感
前端·后端·程序员
聪明的墨菲特i20 分钟前
Django前后端分离基本流程
后端·python·django·web3
hlsd#1 小时前
go mod 依赖管理
开发语言·后端·golang
陈大爷(有低保)1 小时前
三层架构和MVC以及它们的融合
后端·mvc
亦世凡华、1 小时前
【启程Golang之旅】从零开始构建可扩展的微服务架构
开发语言·经验分享·后端·golang
河西石头1 小时前
一步一步从asp.net core mvc中访问asp.net core WebApi
后端·asp.net·mvc·.net core访问api·httpclient的使用
2401_857439691 小时前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧6661 小时前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
阿华的代码王国2 小时前
【SpringMVC】——Cookie和Session机制
java·后端·spring·cookie·session·会话