OpenFeign
大佬文章,请优先查看:
1.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 传参确保消费者和提供者的参数列表一致 包括返回值 方法签名要一致
-
通过 URL 传参数,GET 请求,参数列表使用@PathVariable(“”)
-
如果是 GET 请求,每个基本参数必须加@RequestParam(“”)
-
如果是 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 个小时,是因为时区造成的。
处理方案:
-
使用字符串传递参数,接收方转换成时间类型(推荐使用)不要单独传递时间
-
使用 JDK8 的 LocalDate(日期) 或 LocalDateTime(日期和时间,接收方只有秒,没有毫秒)
-
自定义转换方法
传参总结:
get 请求只用来传递基本参数 而且加注解@RequestParam
post 请求用来传递对象参数 并且加注解@RequestBody
7.OpenFeign 源码分析
7.1 OpenFeign 的原理是什么?
根据上面的案例,我们知道 feign 是接口调用,接口如果想做事,必须要有实现类,可是我们并没有写实现类,只是加了一个@FeignClient(value="xxx-service")的注解。
所以我们猜测 feign 帮我们创建了代理对象,然后完成真实的调用。
动态代理 1jdk (invoke) 2cglib 子类继承的
-
给接口创建代理对象(启动扫描)
-
代理对象执行进入 invoke 方法
-
在 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提醒: 出于性能方面的考虑,我们可以选择用权重策略或区域敏感策略来替代轮询策略,因为这样的执行效率最高。
本文来自博客园,作者:Lz_蚂蚱,转载请注明原文链接:https://www.cnblogs.com/leizia/p/17473432.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步