提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
OpenFeign
本文基于springcloud2021.0.5、springboot 2.6.13,spring-cloud-starter-openfeign 3.1.8
基本使用
使用feign首先要了解@EnableFeignClients
和@FeignClient
这两个注解:
@EnableFeignClients
- @EnableFeignClients 默认扫描 xxxxApplication启动入口 所在包路径下的@FeignClient bean,若无法扫描到,可以在使用Feign调用外部模块的api时候,需要在引用服务中 xxxxApplication 中的
@EnableFeignClients(basePackages = "xxx.xx.xx")
添加外部包需要扫描FeignClient的路径,否则无法注入Bean - @FeignClient 声明的类,使用 spring.application.name 作为 name配置 @FeignClient(name=“xxxx”),如果想保留 context-path , 则需要配置 path 属性 ,如:@FeignClient(name=“xxxx” , path=“xxxx”),这个path值一般是context-path
- @FeignClient接口对应的实现类,需要使用 @RestController注解 声明
- 不支持@GetMapping,@PostMapping等注解,参数要加 @RequestParam(“xxx”)
- FeignClient 调用,实质是httpClient调用 ,若我们暴露的接口api,声明了对应的 http mapper 信息,在调用方调用时候,通过代理 发起了 http请求,到服务提供方对应的http服务上去,所以在服务提供方的接口,可以使用 @RestController来声明接口的 实现,否则调用方无法找到 http 对应的路径,会报404 ; 或者 根据 api 声明的http 信息,构建一个 Controller ,再来调用接口的实现类,但是这样不太方便;
@FeignClient
@FeignClient 标记要用Feign来请求的接口
@FeignClient(name = "payment-service", path = "/api/payment")
public interface FeignPaymentService {
@PostMapping("/create")
Map<String, Object> createPayment(Map<String, Object> map);
}
定义被调用方接口,返回自身的端口号
@RestController
@RequestMapping("/api/payment")
public class PaymentController {
@Value("${server.port}")
private Integer port;
@PostMapping("/create")
public Map<String, Object> create(Map<String, Object> map) {
map.put("port", this.port);
return map;
}
}
name/value
指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
contextId
如果提供了bean名称,则此名称将用作bean的名称而不是默认的name,但它不会被用作服务ID。
这个ID用于在Spring容器中区分不同的Feign客户端实例。在微服务架构中,一个服务可能会调用多个其他服务,而这些服务可能由不同的Feign客户端代理。通过使用contextId,可以确保这些Feign客户端在Spring容器中具有唯一的标识,从而避免潜在的冲突和混淆。
默认情况下每个FeignClient注册进容器的Bean名称是name值+“.FeignClientSpecification”,比如payment-service.FeignClientSpecification
比如有个user服务,但user服务中有很多个接口,不想将所有的调用接口都定义在一个类中,比如
@FeignClient(name = "payment-service")
public interface PaymentClient1 {
@GetMapping("/api/payment/create")
public Map<String, Object> createPayment(Map<String, Object> map);
}
@FeignClient(name = "payment-service")
public interface PaymentClient2 {
@GetMapping("/api/payment/getById")
public Map<String, Object> getPaymentById(@RequestParam("id") int id);
}
此时就会冲突,启动时就会报错
The bean ‘payment-service.FeignClientSpecification’ could not be registered. A bean with that name has already been defined and overriding is disabled.
此时就可以用这个conextId,加了这个注册的bean名称就是conextId值拼上.FeignClientSpecification
或者通过spring.main.allow-bean-definition-overriding=true
允许出现beanName一样的BeanDefinition
qualifiers
FeignClient的@Qualifiers值,如果qualifier()和qualifiers()同时存在,则使用qualifiers()配置, 除非qualifiers()返回空或者只包含null或者空白字符,这时会使用qualifier()配置,如果此时qualifier()不存在,则会取默认值contextId + “FeignClient”.
如果有两个类型相同的FeignClient(类型相同,必须在不同的包),可以通过指定qualifiers注入指定的FeignClient
@FeignClient(contextId = "pc1", name = "payment-service", qualifiers = {"p1"})
public interface FeignPaymentService
@FeignClient(contextId = "pc2", name = "payment-service", qualifiers = {"p2"})
public interface FeignPaymentService
@Autowired
@Qualifier("p1")
FeignPaymentService paymentService;
url
一般用于调试,可以手动指定@FeignClient调用的地址 decode404:当发生http404错误时,如果该字段为true,会调用decoder进行解码,否则抛出FeignException
decode404
decode404控制当Feign客户端接收到HTTP 404响应时的行为。
当decode404设置为true时,如果Feign客户端收到了HTTP 404响应,它会尝试调用配置的Decoder来解码这个响应体。这意味着,尽管请求的资源在服务器上不存在(即404状态),但Feign仍然会尝试将响应体转换为客户端期望的格式,以便进一步处理或抛出更具体的异常。
当decode404设置为false(默认值)时,如果Feign客户端收到HTTP 404响应,它将不会尝试解码响应体,而是直接抛出一个FeignException异常。这通常用于快速失败,因为404错误通常表示客户端请求了一个不存在的资源,无需进一步处理响应体。
配置decode404的行为可以根据应用的需求和错误处理策略进行调整。例如,如果应用需要根据404响应体中的信息来执行特定的逻辑(比如记录日志、返回特定的用户消息等),那么可以将decode404设置为true。否则,如果应用对404错误的处理较为简单,只是需要知道资源不存在,那么保持decode404的默认值(false)可能更为合适。
configuration
Feign配置类,可以自定义Feign的feign.codec.Encoder、feign.codec.Decoder、LogLevel、feign.Contract
fallback
指定发送异常调用或者超时时应该调用那个类来执行备用方法
定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
fallbackFactory
用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的异常熔断处理逻辑,减少重复的代码
path
path表示当前FeignClient的路径的统一前缀,这个 path 就是对应被调用服务的 context-path 值。如果保留服务的 servlet.context-path 配置,则需要指定path值为 servlet.context-path 值
例如,要调用的服务URL是:http://localhost:8080/remote/test/show,其中 /remote 为该服务的 context-path,那么这个时候 path 属性就应该为 /remote,即path+方法上的Mapping路径 = 请求的接口地址
primary
是否将Feign代理标记为主bean。默认为true
使用配置
单个 Feign 配置方式使用@FeignClient的configuration属性,指定一个配置类,如下所示
@Configuration
public class FooConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
// 拦截器配置
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
}
单个配置(yml形式),通过 feignName 应用于指定的 FeignClient 实例。
feign:
client:
config:
feignName: # 指定Feign的名称,及@FeignClient的name属性值
# HTTP 连接超时时间
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
# 重试策略
retryer: com.example.SimpleRetryer
# 请求拦截器
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
# 编码器
encoder: com.example.SimpleEncoder
# 解码器
decoder: com.example.SimpleDecoder
请求拦截器
feign 它自带的拦截器 feign.RequestInterceptor 只能拦截请求, 做一些修改请求头、请求参数之类的动作。没办法将请求和响应一起拦截。
在原生 feign 使用过程中,拦截器通过FeignBuilder添加
Bank bank = Feign.builder()
.decoder(accountDecoder)
.requestInterceptor(new ForwardedForInterceptor())
.target(Bank.class, "https://api.examplebank.com");
使用Spring的话,可以直接RequestInterceptor注册进容器
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String path = requestTemplate.path();
String url = requestTemplate.url();
}
}
请求重试
feign接口调用超时默认会有重试机制
通过配置feign.Retryer实例可以修改重试的时间间隔,最大重试次数等
@Bean
public Retryer feignRetryer(){
// 参数依次为:时间间隔, 最大时间间隔, 最大重试次数
return new Retryer.Default(100, 1000, 4);
}
负载均衡
同时运行4个payment-service服务
注册中心中可以看到有4个payment-service服务
可以看到每次调用返回的端口号都不同,同时8001-8004轮流出现
底层就是这个依赖起作用
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
问题记录
1.接口可以调通,但是调用方这边报404错误
feign.FeignException$NotFound: [404] during [POST] to [http://payment-service/api/payment/create] [FeignPaymentService#createPayment(Map)]
TODO