Spring Cloud OpenFeign
# OpenFeign 简介
OpenFeign 是 SpringCloud 服务调用中间件,可以帮助代理服务 API 接口。并且可以解析 SpringMVC 的 @RequestMapping 注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
Cloud 官网介绍 Feign (opens new window)
OpenFeign 源码:https://github.com/OpenFeign/feig (opens new window)
# OpenFeign能干什么
Java 当中常见的 Http 客户端有很多,除了 Feign,类似的还有 Apache 的 HttpClient 以及 OKHttp3,还有 SpringBoot 自带的 RestTemplate 这些都是 Java 当中常用的 HTTP 请求工具。
什么 是Http 客户端?
当我们自己的后端项目中需要调用别的项目的接口的时候,就需要通过 Http 客户端来调用。在实际开发当中经常会遇到这种场景,比如微服务之间调用,除了微服务之外,可能有时候会涉及到对接一些第三方接口也需要使用到 Http 客户端来调用第三方接口。
所有的客户端相比较,Feign 更加简单一点,在 Feign 的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是 Dao 接口上面标注 Mapper 注解,现在是一个微服务接口上面标注一个 Feign 注解即可),即可完成对服务提供方的接口绑定。
# OpenFeign 和 Feign 的区别
Feign 是 Spring Cloud 组件中的一个轻量级 RESTful 的 HTTP 服务客户端,Feign 内置了 Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign 的使用方式是:使用 Feign 的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
2
3
4
OpenFeign 是 Spring Cloud 在 Feign 的基础上支持了 SpringMVC 的注解,如 @RequesMapping
等等。OpenFeign 的 @FeignClient
可以解析 SpringMVC 的 @RequestMapping
注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2
3
4
Feign是在 2019 就已经不再更新了,通过 Maven 网站就可以看出来,随之取代的是 OpenFeign,从名字上就可以知道,他是 Feign 的升级版。
# OpenFeign 原理
在启动类添加 @EnableFeignClients
注解开启对 @FeignClient
注解的扫描加载处理。根据 Feign Client 的开发规范,定义接口并添加 @FeiginClient
注解。
当程序启动之后,会进行包扫描,扫描所有 @FeignClient
注解的接口,并将这些信息注入到 IOC 容器中。当定义的 Feign 接口被调用时,通过 JDK 的代理的方式生成具体的 RequestTemplate。Feign 会为每个接口方法创建一个 RequestTemplate 对象。该对象封装了 HTTP 请求需要的所有信息,例如请求参数名、请求方法等信息。
然后由 RequestTemplate 生成 Request,把 Request 交给 Client 去处理,这里的 Client 可以是 JDK 原生的 URLConnection、HttpClient 或 Okhttp。最后 Client 被封装到 LoadBalanceClient 类,看这个类的名字既可以知道是结合 Ribbon 负载均衡发起服务之间的调用,因为在 OpenFeign 中默认是已经整合了 Ribbon 了,当然现在推荐使用 loadbalancer 代替 Ribbon。
# @FeignClient
使用 OpenFeign 就一定会用到这个注解,@FeignClient
属性如下:
name:指定该类的容器名称,类似于
@Service
(容器名称)url: url 一般用于调试,可以手动指定
@FeignClient
调用的地址decode404:当发生 Http 404 错误时,如果该字段位 true,会调用 decoder 进行解码,否则抛出 FeignException
configuration: Feign 配置类,可以自定义 Feign 的 Encoder、Decoder、LogLevel、Contract
fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback 指定的类必须实现
@FeignClient
标记的接口fallbackFactory: 工厂类,用于生成 fallback 类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
path: 定义当前 FeignClient 的统一前缀,当我们项目中配置了
server.context-path
,server.servlet-path
时使用
将 FeignClient 注入到 Spring 容器当中
@FeignClient(name = "feignTestService", url = "http://localhost:8080")
public interface FeignTestService {
}
2
3
# OpenFeign 使用
导入依赖
父 pom.xml:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
2
3
4
5
6
7
用到 OpenFeign 的子 pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
2
3
4
5
6
7
8
9
10
11
12
13
14
这里不再用到 Ribbon,因为它已经不再更新,官方取而代之的是 loadbalancer。
启动类需要添加
@EnableFeignClients
这是开启 OpenFeign 客户端的扫描,也就是告诉 OpenFeign,项目里有 OpenFeign 客户端。
@SpringBootApplication
@EnableFeignClients
public class OpenFiegnServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OpenFiegnServiceApplication.class, args);
}
}
2
3
4
5
6
7
# 常规远程调用
所谓常规远程调用,指的是对接第三方接口,和第三方并不是微服务模块关系,所以肯定不可能通过注册中心来调用服务。
假设有一个项目 A 的 Controller 接口类:
@RestController
@RequestMapping("/payment")
public class FeignTestController {
@GetMapping("/selectPaymentList")
public HttpResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize) {
System.out.println(pageIndex);
System.out.println(pageSize);
Payment payment = new Payment();
payment.setSerial("222222222");
return new CommonResult(200, "查询成功, 服务端口:" + payment);
}
@GetMapping(value = "/selectPaymentListByQuery")
public HttpResult<Payment> selectPaymentListByQuery(Payment payment) {
System.out.println(payment);
return new CommonResult(200, "查询成功, 服务端口:" + null);
}
@PostMapping(value = "/create", consumes = "application/json")
public HttpResult<Payment> create(@RequestBody Payment payment) {
System.out.println(payment);
return new CommonResult(200, "查询成功, 服务端口:" + null);
}
@GetMapping("/getPaymentById/{id}")
public HttpResult<Payment> getPaymentById(@PathVariable("id") String id) {
System.out.println(id);
return new CommonResult(200, "查询成功, 服务端口:" + null);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
另一个项目 B 想使用项目 A 的接口数据,则用 OpenFeign 发请求,OpenFeign 客户端是接口,这也是它的特点,通过在接口方法添加 Spring 的注解来实现请求。
@FeignClient(name = "feignTestService", url = "http://localhost:8080")
public interface FeignTestService {
@GetMapping(value = "/payment/selectPaymentList")
HttpResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);
@GetMapping(value = "/payment/selectPaymentListByQuery")
HttpResult<Payment> selectPaymentListByQuery(@SpringQueryMap Payment payment);
@PostMapping(value = "/payment/create", consumes = "application/json")
HttpResult<Payment> create(@RequestBody Payment payment);
@GetMapping("/payment/getPaymentById/{id}")
HttpResult<Payment> getPaymentById(@PathVariable("id") String id);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HttpResult 是一个通用封装类,这里只是演示,具体要根据实际的返回值来指定返回类型,可以是 Map,也可以是 String,也可以是自定义的实体类。而 HttpResult 在这里是所有接口返回的时候都统一用的,所以 OpenFeign 才能解析。
这里的 name 是一个该 OpenFeign 客户端的名字而已,而请求完成的 URL 等于 FeignClient
上的 url + 方法上注解的 value。
# @SpringQueryMap注解
Spring Cloud 项目使用 Feign 的时候都会发现一个问题,就是 get 方式无法解析对象参数。其实 Feign 是支持对象传递的,但是得是 Map 形式,而且不能为空,与 Spring 在机制上不兼容,因此无法使用。Spring Cloud 在 2.1.x 版本中提供了 @SpringQueryMap
注解,可以传递对象参数,框架自动解析。
# 微服务使用步骤
微服务之间使用 OpenFeign,肯定是要通过注册中心来访问服务的。提供者将自己的 IP + 端口号注册到注册中心,然后对外提供一个服务名称,消费者根据服务名称去注册中心当中寻找 IP 和端口。
假设 常规远程调用 项目 A 是一个注册到注册中心的微服务,项目名是 payment-service,如果配置项目名,在 application.yml 添加
spring:
application:
name: payment-service
2
3
项目 B 想调用项目 A 的接口,除了上面一样写项目 A 的 URL 外,可以直接写项目名代表 URL。
@FeignClient(name = "payment-service", url = "http://localhost:8080")
public interface FeignTestService {
@GetMapping(value = "/payment/selectPaymentList")
HttpResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);
@GetMapping(value = "/payment/selectPaymentListByQuery")
HttpResult<Payment> selectPaymentListByQuery(@SpringQueryMap Payment payment);
@PostMapping(value = "/payment/create", consumes = "application/json")
HttpResult<Payment> create(@RequestBody Payment payment);
@GetMapping("/payment/getPaymentById/{id}")
HttpResult<Payment> getPaymentById(@PathVariable("id") String id);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
好像和 常规远程调用 的一样,只是改了个名字?
因为 OpenFeign 要求 @FeignClient
的 url 是必填的,所以随便填就可以了,请求的时候不会用到的,因为项目 B 一旦注册到注册中心,则走 name 去找其他注册中心的项目,没有注册才走 url。
在 常规远程调用 中,name 只是一个名字代号,在微服务里,它就是目标微服务的项目名,这里就是 payment-service,
当然也可以用 value 代替 name,在微服务里,两者功能一样。
@FeignClient(value = "payment-service", url = "http://localhost:8080")
public interface FeignTestService {
@GetMapping(value = "/payment/selectPaymentList")
HttpResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);
// ......
}
2
3
4
5
6
7
8
当然如果 payment-service 是一个集群的微服务,则走 loadbalancer 负载均衡的轮询。
# OpenFeign 添加 Header
以下提供了四种方式:
在
@RequestMapping
中添加
@FeignClient(value = "payment-service", url = "http://localhost:8080")
public interface FeignTestService {
@PostMapping(value = "/payment/create", headers = {"Content-Type=application/json;charset=UTF-8"})
List<String> create(@RequestParam("names") String[] names);
}
2
3
4
5
在方法参数前面添加
@RequestHeader
注解
@FeignClient(value = "payment-service", url = "http://localhost:8080")
public interface FeignTestService {
@PostMapping(value = "/payment/create", consumes = "application/json")
List<String> create(@RequestParam("names") String[] names, @RequestHeader("Authorization") String token);
}
2
3
4
5
此时 token 的值就是请求头 Authorization 的值。
设置多个属性时,可以使用 Map
@FeignClient(value = "payment-service", url = "http://localhost:8080")
public interface FeignTestService {
@PostMapping(value = "/payment/create", consumes = "application/json")
List<String> create(@RequestParam("names") String[] names, @RequestHeader MultiValueMap<String, String> headersn);
}
2
3
4
5
使用 @Header 注解
@FeignClient(value = "payment-service", url = "http://localhost:8080")
public interface FeignTestService {
@PostMapping(value = "/payment/create", consumes = "application/json")
@Headers({"Content-Type: application/json;charset=UTF-8"})
List<String> create(@RequestParam("names") String[] names);
}
2
3
4
5
6
实现 RequestInterceptor 接口(拦截器)
只要通过该 FeignClient 访问的接口都会走这个地方,所以使用的时候要注意一下,它是作用在该 FeignClient 的所有请求里。
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate temp) {
temp.header(HttpHeaders.AUTHORIZATION, "XXXXX");
}
}
2
3
4
5
6
7
然后在 FeignClient 使用:
@FeignClient(value = "payment-service", url = "http://localhost:8080", configuration = FeignRequestInterceptor.class)
public interface FeignTestService {
@GetMapping(value = "/payment/selectPaymentList")
HttpResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);
@GetMapping(value = "/payment/selectPaymentListByQuery")
HttpResult<Payment> selectPaymentListByQuery(@SpringQueryMap Payment payment);
@PostMapping(value = "/payment/create", consumes = "application/json")
HttpResult<Payment> create(@RequestBody Payment payment);
@GetMapping("/payment/getPaymentById/{id}")
HttpResult<Payment> getPaymentById(@PathVariable("id") String id);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这四个请求都走 FeignRequestInterceptor,添加请求头。
# 动态 URL
前面我们都是在 @FeignClien
写 url,然后利用 @RequestMapping
写 url,最后拼接成完成的请求地址,但是会有一种场景就是请求的 URL 无法知道,是从数据库或者前端传来的 URL,此时它是一个动态 URL。
所以我们无法使用上面这些功能拼接处理,而是直接将要请求的 URL 传给 OpenFeign。
@FeignClient(name = "generic-feign", url = "http://localhost:8080")
public interface GenericFeign {
@PostMapping(consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
Map<String, String> doPostToken(URI uri, @RequestBody Map<String, ?> body);
}
2
3
4
5
6
doPostToken 方法第一个参数是 URI uri
,它就是 OpenFeign 要请求的 URL。
当我们在方法的第一个参数提供了 URI 对象,则不走 @FeignClient
的 url 和 @RequestMapping
的 url 拼接,而是直接走 URI 对象提供的 url。
但是前面说了,@FeignClient
的 url 是必填的,所以我们随便填一个地址就可以了,最终会被 URI 替代。
调用 doPostToken 方法:
genericFeign.doPostToken(new URI("http://www.youngkbt.cn/test"), authParamMap);
此时 GenericFeign 的 doPostToken 将请求 http://www.youngkbt.cn/test
。
# 其他功能
# OpenFeign 日志打印
OpenFeign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 OpenFeign 中 Http 请求的细节。
说白了就是 对 OpenFeign 接口的调用情况进行监控和输出。
日志级别:
NONE:默认的,不显示任何日志
BASIC:仅记录请求方法、URL、响应状态码及执行时间
HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息
FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据
配置日志 Bean:
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
2
3
4
5
6
7
8
9
10
11
yml 文件里需要开启日志的 Feign 客户端
logging:
level:
cn.youngkbt.service.PaymentFeignService: debug
2
3
OpenFiegn 依赖 log 日志,且通过源码可以发现,必须开启 log 的 debug 级别,才会开启 OpenFeign 日志。
# OpenFeign 超时控制
当项目 B 调用项目 A 的接口时,项目 A 迟迟不给内容,不响应,则项目 B 会产生请求超时问题。我们可以设置 OpenFeign 的超时时间。
OpenFeign 默认等待 1 秒钟,超过后报错,所以我们可能扩大这个时间到 10 秒,在 application.yml 配置:
feign:
client:
config:
default:
read-timeout: 10000
connect-timeout: 10000
2
3
4
5
6
# OpenFeign 容错降级
正如同 OpenFeign 超时控制所讲,当超时或者接口出现异常的时候,请求的状态不再是 200,则我们可以对 OpenFeign 进行降级处理,返回一个友好的信息给用户,而不是内部消化接口异常。
这里用到 alibaba 的 Sentinel,不再用到 Hystrix,尽管它的设计理念依然先进,但是官方不再维护升级,所以我们就使用新一代服务降级 Sentinel。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2
3
4
然后在 application.yml 开启 Sentinel
feign:
sentinel:
enabled: true
2
3
此时就需要对 OpenFeign 客户端的每一个方法提供降级处理,这里提供一个降级处理类。
@Component
@Slf4j
public class FeignTestFallbackFactory implements FallbackFactory<FeignTestService> {
@Override
public FeignTestService create(Throwable cause) {
return new FeignTestService() {
@Override
public HttpResult<Payment> selectPaymentList(int pageIndex, int pageSize) {
log.info("selectPaymentList request fail");
return HttpResult.fail("selectPaymentList request fail");
}
@Override
public HttpResult<Payment> selectPaymentListByQuery(Payment payment) {
log.info("selectPaymentListByQuery request fail");
return HttpResult.fail("selectPaymentListByQuery request fail");
}
@Override
public HttpResult<Payment> create(Payment payment) {
log.info("create request fail");
return HttpResult.fail("create request fail");
}
@Override
public HttpResult<Payment> getPaymentById(String id) {
log.info("getPaymentById request fail");
return HttpResult.fail("getPaymentById request fail");
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
然后在 OpenFeign 的 @FeignClient
使用 configuration:
@FeignClient(name = "payment-service", url = "http://localhost:8080", configuration = FeignTestFallbackFactory.class)
public interface FeignTestService {
@GetMapping(value = "/payment/selectPaymentList")
HttpResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);
@GetMapping(value = "/payment/selectPaymentListByQuery")
HttpResult<Payment> selectPaymentListByQuery(@SpringQueryMap Payment payment);
@PostMapping(value = "/payment/create", consumes = "application/json")
HttpResult<Payment> create(@RequestBody Payment payment);
@GetMapping("/payment/getPaymentById/{id}")
HttpResult<Payment> getPaymentById(@PathVariable("id") String id);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
此时如果任意一个方法请求时出现非 200 状态,则走 FeignTestFallbackFactory 里对应的方法。返回内容。
当然也可以读取异常信息,进行日志记录等等。
# 手动创建 Feign 客户端
@FeignClient
无法支持同一 service 具有多种不同配置的 FeignClient,因此,在必要时需要手动 build FeignClient。
@FeignClient(value = "payment-service")
以这个为例,假如出现两个服务名称为 payment-service 的 FeignClient,项目直接会启动报错,但是有时候我们服务之间调用的地方较多,不可能将所有调用都放到一个 FeignClient 下,这时候就需要自定义来解决这个问题。
官网当中也明确提供了自定义 FeignClient,以下是在官网基础上对自定义 FeignClient 的一个简单封装,供参考。
首先创建 FeignClientConfigurer 类,这个类相当于 build FeignClient 的工具类。
import feign.*;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.slf4j.Slf4jLogger;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Import;
@Import(FeignClientsConfiguration.class)
public class FeignClientConfigurer {
private Decoder decoder;
private Encoder encoder;
private Client client;
private Contract contract;
public FeignClientConfigurer(Decoder decoder, Encoder encoder, Client client, Contract contract) {
this.decoder = decoder;
this.encoder = encoder;
this.client = client;
this.contract = contract;
}
public RequestInterceptor getUserFeignClientInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
// 添加 header
}
};
}
public <T> T buildAuthorizedUserFeignClient(Class<T> clazz, String serviceName) {
return getBasicBuilder().requestInterceptor(getUserFeignClientInterceptor())
// 默认是 Logger.NoOpLogger
.logger(new Slf4jLogger(clazz))
// 默认是 Logger.Level.NONE(一旦手动创建 FeignClient,全局配置的 logger 就不管用了,需要在这指定)
.logLevel(Logger.Level.FULL)
.target(clazz, buildServiceUrl(serviceName));
}
private String buildServiceUrl(String serviceName) {
return "http://" + serviceName;
}
protected Feign.Builder getBasicBuilder() {
return Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
使用工具类的方法创建多个 FeignClient 配置
import com.gzl.cn.service.FeignTest1Service;
import feign.Client;
import feign.Contract;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignClientConfiguration extends FeignClientConfigurer {
public FeignClientConfiguration(Decoder decoder, Encoder encoder, Client client, Contract contract) {
super(decoder, encoder, client, contract);
}
@Bean
public FeignTest1Service feignTest1Service() {
return super.buildAuthorizedUserFeignClient(FeignTest1Service.class, "payment-service");
}
// 假如多个 FeignClient 在这里定义即可
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
其中,super.buildAuthorizedUserFeignClient()
方法中,第一个参数为调用别的服务的接口类,第二个参数为被调用服务在注册中心的 service-id。
public interface FeignTest1Service {
@GetMapping(value = "/payment/get/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
2
3
4
5
使用的时候正常注入使用即可
@Resource
private FeignTest1Service feignTest1Service;
2
# Feign 继承支持
Feign 通过单继承接口支持样板 API。这允许将常用操作分组到方便的基本接口中。
UserService.java
public interface UserService {
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
User getUser(@PathVariable("id") long id);
}
2
3
4
UserClient.java
@FeignClient(name = "user-service", url = "http://localhost:8080")
public interface UserClient extends UserService {
}
2
3
4
UserClient 作为一个 FiegnClient,集成得到了 getUser 请求。