OpenFeign

大佬文章,请优先查看:

服务调用/通信-OpenFeign最佳实践

1.OpenFeign 简介

OpenFeign官网文档

Spring Cloud OpenFeign 它是 Spring 官方推出的一种声明式服务调用与负载均衡组件。它底层基于 Netflix Feign,Netflix Feign 是 Netflix 设计的开源的声明式 WebService 客户端,用于简化服务间通信。

Spring Cloud openfeign 对 Feign 进行了增强,使其支持 Spring MVC 注解,另外还整合了 Ribbon 和 Nacos,从而使得 Feign 的使用更加方便。

Feign 是声明性(注解)Web 服务客户端。它使编写 Web 服务客户端更加容易。要使用 Feign,请创建一个接口并对其进行注解。它具有可插入注解支持,包括 Feign 注解和 JAX-RS 注解。

Feign 还支持可插拔编码器和解码器。Spring Cloud 添加了对 Spring MVC 注解的支持,并支持使用 HttpMessageConverters,Spring Web 中默认使用的注解。Spring Cloud 集成了 Ribbon 和 Eureka 以及 Spring Cloud LoadBalancer,以在使用 Feign 时提供负载均衡的 http 客户端。

Feign 是一个远程调用的组件,Feign 集成了 ribbon,ribbon 里面集成了 eureka。

2.OpenFeign 快速入门

feign服务提供者

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hguo</groupId>
<artifactId>feign-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>feign-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
server:
port: 8081
spring:
application:
name: openFeign-server
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

添加访问接口

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 订单控制器
*
* @author leizi
* @create 2023-04-20 23:44
*/
@RestController
public class OrderController {
/**
* 添加订单
*
* @param orderName 订单名称
* @return
*/
@GetMapping("/add")
public String addOrder(@RequestParam("orderName") String orderName) {
System.out.println("create order:" + orderName);
return "success";
}
}

启动测试访问

feign服务消费者

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hguo</groupId>
<artifactId>feign-client</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>feign-client</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!--OpenFeign依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

application.yml

server:
port: 8082
spring:
application:
name: openFeign-client
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

添加feign接口

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author leizi
* @create 2023-05-04 20:43
*/
// value = "openFeign-server", value后面值必须是和提供者服务名一致
@FeignClient(value = "openFeign-server")
public interface OrderFeign {
@GetMapping("/add")
String addOrder(@RequestParam("orderName") String orderName);
}

添加访问接口

/**
* @author leizi
* @create 2023-05-04 20:46
*/
@RestController
public class UserController {
@Autowired
private OrderFeign orderFeign;
/**
* 根据用户id添加订单
*
* @param userId 用户id
* @return
*/
@GetMapping("/user/{userId}")
public String addOrderByUserId(@PathVariable Integer userId) {
return "根据用户id添加订单:" + orderFeign.addOrder(String.valueOf(userId));
}
}

配置启动类,在启动类上加上@EnableFeignClients注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // 标记feign客户端
@EnableEurekaClient
public class FeignClientApplication {
public static void main(String[] args) {
SpringApplication.run(FeignClientApplication.class, args);
}
}

访问测试

本次调用总结

@FeignClient 注解说明当前接口为 OpenFeign 通信客户端,参数值 openFeign-server 为服务提供者 ID(注意,OpenFeign服务名称不支持下划线_,这是一个坑),这一项必须与 注册中心中 注册 ID 保持一致。

在 OpenFeign 发送请求前会自动在 注册中心 查询 openFeign-server 所有可用实例信息,再通过内置的 Ribbon 负载均衡选择一个实例发起 RESTful 请求,进而保证通信高可用。

3.测试 feign 调用的负载均衡

启动多台 provider-order-service:

测试访问:

4.@FeignClient 注解属性详解

contextId: 如果配置了contextId,该值将会作为beanName。

fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口

fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码

url: url一般用于调试,可以手动指定@FeignClient调用的地址

5.调用超时设置

因为 ribbon 默认调用超时时长为1s ,可以修改,超时调整可以查看DefaultClientConfigImpl

server:
port: 8082
spring:
application:
name: feign-client
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
# feign只是帮你封装了远程调用的功能 底层还是ribbon 所以我们需要去修改ribbon的时间
ribbon:
ReadTimeout: 3000 # 3s超时时间
ConnectTimeout: 3000 # 链接服务的超时时间
logging:
level:
com.hguo.feignclient.feign.OrderFeign: debug # 打印这个接口下面的日志

6.OpenFeign 调用参数处理

Feign 传参确保消费者和提供者的参数列表一致 包括返回值 方法签名要一致

  1. 通过 URL 传参数,GET 请求,参数列表使用@PathVariable(“”)

  2. 如果是 GET 请求,每个基本参数必须加@RequestParam(“”)

  3. 如果是 POST 请求,而且是对象集合等参数,必须加@Requestbody 或者@RequestParam

import com.powernode.domain.Order;
import org.springframework.web.bind.annotation.*;
import javax.annotation.PostConstruct;
import java.util.Date;
/**
* url /doOrder/热干面/add/油条/aaa
* get传递一个参数
* get传递多个参数
* post传递一个对象
* post传递一个对象+一个基本参数
*/
@RestController
public class ParamController {
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age) {
System.out.println(name + ":" + age);
return "ok";
}
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false) String name) {
System.out.println(name);
return "ok";
}
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age) {
System.out.println(name);
System.out.println(age);
return "ok";
}
@PostMapping("oneObj")
public String oneObj(@RequestBody Order order) {
System.out.println(order);
return "ok";
}
@PostMapping("oneObjOneParam")
public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name) {
System.out.println(name);
System.out.println(order);
return "ok";
}
/**
单独传递时间对象
*/
@GetMapping("testTime")
public String testTime(@RequestParam Date date){
System.out.println(date);
return "ok";
}
}

Feign 接口

import com.powernode.domain.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
/**
* @FeignClient(value = "order-service")
* value 就是提供者的应用名称
*/
@FeignClient(value = "order-service")
public interface UserOrderFeign {
/**
* 你需要调用哪个controller 就写它的方法签名
* 方法签名(就是包含一个方法的所有的属性)
*
* @return
*/
@GetMapping("doOrder")
String doOrder();
@GetMapping("testUrl/{name}/and/{age}")
public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age);
@GetMapping("oneParam")
public String oneParam(@RequestParam(required = false) String name);
@GetMapping("twoParam")
public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age);
@PostMapping("oneObj")
public String oneObj(@RequestBody Order order);
@PostMapping("oneObjOneParam")
public String oneObjOneParam(@RequestBody Order order, @RequestParam("name") String name);
@GetMapping("testTime")
public String testTime(@RequestParam Date date);
}

创建 TestController 类

import com.powernode.domain.Order;
import com.powernode.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
@RestController
public class UserController {
/**
* 接口是不能做事情的
* 如果想做事 必须要有对象
* 那么这个接口肯定是被创建出代理对象的
* 动态代理 jdk(java interface 接口 $Proxy ) cglib(subClass 子类)
* jdk动态代理 只要是代理对象调用的方法必须走 java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[])
*/
@Autowired
public UserOrderFeign userOrderFeign;
/**
* 总结
* 浏览器(前端)-------> user-service(/userDoOrder)-----RPC(feign)--->order-service(/doOrder)
* feign的默认等待时间时1s
* 超过1s就在直接报错超时
*
* @return
*/
@GetMapping("userDoOrder")
public String userDoOrder() {
System.out.println("有用户进来了");
// 这里需要发起远程调用
String s = userOrderFeign.doOrder();
return s;
}
@GetMapping("testParam")
public String testParam(){
String cxs = userOrderFeign.testUrl("cxs", 18);
System.out.println(cxs);
String t = userOrderFeign.oneParam("老唐");
System.out.println(t);
String lg = userOrderFeign.twoParam("雷哥", 31);
System.out.println(lg);
Order order = Order.builder()
.name("牛排")
.price(188D)
.time(new Date())
.id(1)
.build();
String s = userOrderFeign.oneObj(order);
System.out.println(s);
String param = userOrderFeign.oneObjOneParam(order, "稽哥");
System.out.println(param);
return "ok";
}
/**
* Sun Mar 20 10:24:13 CST 2022
* Mon Mar 21 00:24:13 CST 2022 +- 14个小时
* 1.不建议单独传递时间参数
* 2.转成字符串 2022-03-20 10:25:55:213 因为字符串不会改变
* 3.jdk LocalDate 年月日 LocalDateTime 会丢失s
* 4.改feign的源码
*
* @return
*/
@GetMapping("time")
public String time(){
Date date = new Date();
System.out.println(date);
String s = userOrderFeign.testTime(date);
LocalDate now = LocalDate.now();
LocalDateTime now1 = LocalDateTime.now();
return s;
}
}

时间日期参数问题

使用 feign 远程调用时,传递 Date 类型,接收方的时间会相差 14 个小时,是因为时区造成的。

处理方案:

  1. 使用字符串传递参数,接收方转换成时间类型(推荐使用)不要单独传递时间

  2. 使用 JDK8 的 LocalDate(日期) 或 LocalDateTime(日期和时间,接收方只有秒,没有毫秒)

  3. 自定义转换方法

传参总结:
get 请求只用来传递基本参数 而且加注解@RequestParam
post 请求用来传递对象参数 并且加注解@RequestBody

7.OpenFeign 源码分析

7.1 OpenFeign 的原理是什么?

根据上面的案例,我们知道 feign 是接口调用,接口如果想做事,必须要有实现类,可是我们并没有写实现类,只是加了一个@FeignClient(value="xxx-service")的注解。

所以我们猜测 feign 帮我们创建了代理对象,然后完成真实的调用。
动态代理 1jdk (invoke) 2cglib 子类继承的

  1. 给接口创建代理对象(启动扫描)

  2. 代理对象执行进入 invoke 方法

  3. 在 invoke 方法里面做远程调用
    具体我们这次的流程:

A. 扫描注解得到要调用的服务名称和 url

B. 拿到 provider-order-service/doOrder,通过 ribbon 的负载均衡拿到一个服务,
provider-order-service/doOrder --->http://ip:port/doOrder

C. 发起请求,远程调用

7.2 OpenFeign 的内部是如何实现

7.2.1 如何扫描注解@FeignClient

查看启动类的@EnableFeignClients

进入 FeignClientsRegistrar 这个类 去查看里面的东西

真正的扫描拿到注解和服务名称

7.2.2 如何创建代理对象去执行调用?

当我们启动时,在 ReflectiveFeign 类的 newInstance 方法,给接口创建了代理对象。

ReflectiveFeign 类中的 invoke 方法帮我们完成调用

SynchronousMethodHandler 的 invoke 中给每一个请求创建了一个 requestTemplate 对象,去执行请求。

executeAndDecode

我们去看 LoadBalancerFeignClient 的 execute 方法

executeWithLoadBalancer 继续往下看

8.OpenFeign 的日志功能

从前面的测试中我们可以看出,没有任何关于远程调用的日志输出,如请求头,参数,OpenFeign 的调用默认是不打日志的。Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而揭开 Feign 中 Http 请求的所有细节。

8.1OpenFeign 的日志级别

  • NONE 不打日志,默认值
  • BASIC 只记录 method、url、响应码,执行时间
  • HEADERS 只记录请求和响应的 header
  • FULL 全部都记录

8.2 配置类方式

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLogger() {
return Logger.Level.FULL;
}
}

8.3 配置文件方式

logging:
level:
com.hguo.feignclient.feign.OrderFeign: info # 打印这个接口下面的日志
feign:
client:
config:
default: # 项目全局
loggerLevel: HEADERS
order-service: #@FeignClient注解中配置的服务名
loggerLevel: FULL

上面修改了 openfeign 的日志级别是 debug,但是 springboot 默认日志级别是 info,因为 debug<info,所以需要也改为debug,openfeign 的日志才会生效

logging:
level:
com.hguo.feignclient.feign.OrderFeign: debug

9.文件上传

@PostMapping(value = "/upload-file")
public String handleFileUpload(@RequestPart(value = "file") MultipartFile file) {
// File upload logic
}
public class FeignSupportConfig {
@Bean
public Encoder multipartFormEncoder() {
return new SpringFormEncoder(new SpringEncoder(new ObjectFactory<HttpMessageConverters>() {
@Override
public HttpMessageConverters getObject() throws BeansException {
return new HttpMessageConverters(new RestTemplate().getMessageConverters());
}
}));
}
}
@FeignClient(name = "file", url = "http://localhost:8081", configuration = FeignSupportConfig.class)
public interface UploadClient {
@PostMapping(value = "/upload-file", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String fileUpload(@RequestPart(value = "file") MultipartFile file);
}

10.性能优化

替换默认通信组件

OpenFeign 默认使用 Java 自带的 URLConnection 对象创建 HTTP 请求,但接入生产时,如果能将底层通信组件更换为 Apache HttpClient、OKHttp 这样的专用通信组件,基于这些组件自带的连接池,可以更好地对 HTTP 连接对象进行重用与管理。

<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!-- 或者添加 httpclient 框架依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

然后在配置文件中加入如下:

feign:
okhttp:
enabled: true
# 或者
feign:
httpclient:
enabled: true

经过上面设置已经可以使用okhttp了,因为在FeignAutoConfiguration中已实现自动装配

11.数据压缩

在 OpenFeign 中,默认并没有开启数据压缩功能。但如果你在服务间单次传递数据超过 1K 字节,强烈推荐开启数据压缩功能。默认 OpenFeign 使用 Gzip 方式压缩数据,对于大文本通常压缩后尺寸只相当于原始数据的 10%~30%,这会极大提高带宽利用率。,在项目配置文件 application.yml 中添加以下配置:

feign:
compression:
request:
enabled: true # 开启请求数据的压缩功能
mime-types: text/xml,application/xml, application/json # 压缩类型
min-request-size: 1024 # 最小压缩值标准,当数据大于 1024 才会进行压缩
response:
enabled: true # 开启响应数据压缩功能

Tip提醒: 如果应用属于计算密集型,CPU 负载长期超过 70%,因数据压缩、解压缩都需要 CPU 运算,开启数据压缩功能反而会给 CPU 增加额外负担,导致系统性能降低,这是不可取的。这种情况 建议不要开启数据的压缩功能

12.负载均衡

OpenFeign 使用时默认引用 Ribbon 实现客户端负载均衡,它默认的负载均衡策略是轮询策略。那如何设置 Ribbon 默认的负载均衡策略呢?

只需在 application.yml 中调整微服务通信时使用的负载均衡类即可。

warehouse-service: #服务提供者的微服务ID
ribbon:
#设置对应的负载均衡类
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

Tip提醒: 出于性能方面的考虑,我们可以选择用权重策略或区域敏感策略来替代轮询策略,因为这样的执行效率最高。

posted @   Lz_蚂蚱  阅读(90)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起