Spring Cloud(五):声明式 REST 客户端 Feign

  Feign 是一个声明式 Web 客户端,让 Rest 服务调用更简单。要使用 Feign ,只需在编写接口并添加其注解,就可以定义好 http 请求的参数、格式、地址等信息。

  Feign 会完全代理 Http 请求,只需向调用方法一样调用 feign 注解的客户端就可以完成服务请求及相关处理。

  Spring Cloud 支持集成 Ribbon 和 Eureka,Feign 和他们一起使用时支持客户端负载均衡功能。Spring Cloud OpenFeign 文档

Java 项目接口调用

Java 项目中调用接口通常会用到以下工具:

  1. HttpClient:HttpClient 是 Apache 下的子项目,功能丰富,易用,灵活和高效。
  2. Okhttp:一个处理网络请求的开源框架。Okhttp 具有更简洁的 API,高效的性能,并支持多种协议。
  3. Httpurlconnection:是 Java 的标准类,继承自 URLConnection,发送 get/post 请求,使用比较复杂。
  4. RestTemplate:是 Spring 提供的用于访问 Rest 服务的客户端,便捷高效。

Spring Cloud Feign

Spring Cloud 架构项目,在客户端(消费者)应用引入 eureka-client、ribbon、feign 依赖;在 Service 层创建接口文件,接口上使用注解 @FeignClient 指定要调用远程服务的应用名,创建接口方法,根据远程服务接口的调用规则定义此接口(类似于 Controller 层方法的注解定义请求路径、请求类型、参数类型等)

引入 Feign 依赖

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Feign 注解使用

  1. 启用 Feign 客户端:Spring Boot 项目在入口类 Application 上添加启用 Feign 客户端的注解 @EnableFeignClients

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableFeignClients
    public class Application {

    public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
    }
    }
  2. 使用 Feign 客户端:在 Service 层创建接口,在接口上使用 Feign 客户端注解 @FeignClient

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //注解属性还可使用 ${} 方式注入环境变量中配置的值
    //如:@FeignClient(name = "${service.application.name.sakila-service}",path = "/service", configuration = FeignCustomConfig.class)
    @FeignClient(name = "service_name", path = "/")
    public interface StoreClient {
    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    List<Store> getStores();

    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    Store update(@PathVariable("storeId") Long storeId, Store store);
    }

    在 @FeignClient 注解中,name 属性是服务提供者的应用名(由 spring.application.name 属性定义),用于创建 Ribbon 负载均衡器;如果应用是 Eureka 客户端,name 解析的是 Eureka 服务注册表中的服务,如果没有使用 Eureka,则需要在配置文件中配置服务器列表。还可使用 url 属性指定 URL的绝对路径或仅指定主机名。

    应用程序上下文中 Bean 的名称是接口的完全限定名,可以使用 qualifier 属性自定义 Bean 的别名。

  3. 消费者端的 Controller 层注入 @FeignClient注解的 Service 接口,在 Controller 层调用 Service 层接口。

  4. Feign 调用简单示例

    消费者应用:Controller 层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @RestController
    @RequestMapping("/consumer1")
    public class ConsumerController {

    //或使用 @Resource 注解
    @Autowired(required = false)
    private FeignInterface feignInterface;

    @GetMapping("/feign")
    public String feignCall(){
    String str = feignInterface.callHome();
    return str;
    }
    }

    消费者应用:@FeignClient 注解 Service 层接口

    1
    2
    3
    4
    5
    6
    @FeignClient(name = "sakila-service1",path = "/service")
    public interface FeignInterface {

    @GetMapping("/home")
    public String callHome();
    }

    服务提供者:远程服务接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RequestMapping("/service")
    public class HomeController {

    @GetMapping("/home")
    public String home() {
    InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka("sakila-service1", false);
    String hostName = instanceInfo.getHostName();
    int port = instanceInfo.getPort();
    String appName = instanceInfo.getAppName();
    return hostName + port + appName;
    }

    向消费者接口请求:http://localhost:8080/consumer1/feign

FeignClient 重名处理

若在一个服务里定义了多个 name 属性相同的 FeignClient ,直接启动是会报错。如下:

1
@FeignClient(name = "userVipService", path = "/user")

错误:**The bean ‘xxx.FeignClientSpecification’, defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.

原因:有相同名的 FeignClient 客户端重复注册,即 @FeignClient 注解的 name 属性存在重复。

三种解决方案:

  1. 配置多个相同 name 的 Feign Client,使用 @FeignClient注解的 contextId 属性以避免这些配置 Bean 的名称冲突。*– 推荐这种*

  2. 同一个服务的调用,写在同一个 @FeignClient 客户端中,这样 name 就不会重复。

  3. 开启 Spring Bean 允许覆盖,默认是禁止的,如下:

    1
    spring.main.allow-bean-definition-overriding=true

Feign 配置详解

覆盖 Feign 默认配置

Spring Cloud Feign 支持的核心概念是可以指定所要调用的服务(即指定服务名:spring.application.name);一个消费者应用可能有多个 feign 客户端组件构成为一个集合,各个组件按需调用远程服务;该组件集合有一个名称,也可以使用 @FeignClient 注解的 contextId 属性自定义。

Spring Cloud 使用 FeignClientsConfiguration 按需为每个命名客户端创建一个新集合作为 ApplicationContext ,包含了一个 feign.Decoder, 一个 feign.Encoder, 一个 feign.Contract,可以使用 @FeignClient 注解的 contextId 属性重写该集合的名称。

Spring Cloud 允许通过使用 @FeignClient 注解声明其它配置(在 FeignClientsConfiguration 之上) 来完全控制 Feign 客户端。如下:

1
2
3
4
@FeignClient(name = "stores", configuration = FooConfiguration.class)
public interface StoreClient {
//..
}

此情况下,客户端由已经存在于 FeignClientsConfiguration 中的组件和 FooConfiguration 自定义的组件一起组成,后者将覆盖前者。

注意:

  • FooConfiguration 配置类不需要用 @Configuration 注解。如果添加了 @Configuration 注解,则注意将其从任何包含此配置的 @ComponentScan 中排除,否则该配置将成为 feign.Decoder, feign.Encoder, feign.Contract 等的默认源。
    可以将其放在任何 @ComponentScan 或 @SpringBootApplication 单独的非重叠的包中来排除,也可在 @ComponentScan 中指定排除。
  • @FeignClient 注解的 serviceId 属性已过期,使用 name 属性。
  • @FeignClient 注解指定服务,以前使用 url 属性,现在需要使用 name 属性。

nameurl 属性还支持占位符注入环境变量中的值,如下:

1
2
3
4
@FeignClient(name = "${feign.name}", url = "${feign.url}")
public interface StoreClient {
//..
}

Feign 注册的 Bean

Spring Cloud Netflix 默认为 feign 提供以下 Bean,这此 Bean 默认是在 org.springframework.cloud.openfeign.FeignClientsConfiguration 中完成注册的。

BeanType beanName ClassName
Decoder feignDecoder ResponseEntityDecoder
(包装了 SpringDecoder)
Encoder feignEncoder SpringEncoder
Contract feignContract SpringMvcContract
Feign.Builder feignHystrixBuilder HystrixFeign.builder()
Logger feignLogger Slf4jLogger
Client feignClient 如果 Ribbon 可用,则是 LoadBalancerFeignClient
否则使用 Feign 默认客户端

当 feign.okhttp.enabled 或 feign.httpclient.enabled 设置为 true 时,OkHttpClient 或 ApacheHttpClient 注册的 feiClient 将被使用。
若都引入 OkHttpClient 和 ApacheHttpClient 依赖,可以通过注册 Apache ClosableHttpClient Bean 或 OK HTTP OkHttpClient Bean 来自定义 HTTP client 。

HTTP 调用客户端的配置在 Feign 自动配置类 FeignAutoConfiguration 中实现,根据类是否存和属性配置是否启用来装配客户端的 Bean。

默认未提供的 Bean​
Spring Cloud 默认没有为 Feign 提供以下类型的 Bean,但仍然会从应用上下文中查找这些类型的 Bean 来创建 Feign Client。

  • Logger.Level
  • Retryer
  • ErrorDecoder
  • Request.Options
  • Collection<RequestInterceptor>
  • SetterFactory

Feign 自定义 Bean

还可以在 @FeignClient 注解的 configuration 属性指定的配置文件(如, FooConfiguration)中创建自定义的 Bean,允许覆盖默认注册的 Bean。

  1. 自定义 Feign 契约配置 Bean
    原生 Feign 不支持 Spring MVC 注解。Spring Cloud OpenFeign 在 Feign 基础上做了扩展,可以让 Feign 支持 Spring MVC 的注解(如 @RequestMapping)来调用。
    若想在 Spring Cloud 中使用原生的注解方式来定义客户端,可通过修改 Contract 这个配置,Spring Cloud 中默认是 SpringMvcContract。

    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    public class FooConfiguration {
    @Bean
    public Contract feignContract() {
    //替换默认的 SpringMvcContract
    return new feign.Contract.Default();
    }
    }

    当使用默认的 Contract 后,Feign 客户端的接口方法就不能使用 Spring MVC 的注解了。

  2. 自定义 Basic 安全认证 Bean
    通常调用的接口都是有权限控制的,认证信息大多通过参数传递,或通过请求头传递,如 Basic 认证方式。在 Feign 中可直接配置 Basic 认证方式。

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    public class FooConfiguration {
    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
    return new BasicAuthRequestInterceptor("user", "password");
    }
    }

    也可以自定义属于自己的认证方式,其实就是自定义一个拦截器,实现 RequestInterceptor 接口,在 apply() 方法中执行请求之前的认证操作,然后往请求头中设置认证之后的信息。然后在配置类中声明自定义的认证拦截器为 Bean。

Feign 属性配置

  1. @FeignClient 也可以合用属性配置进行配置
    application.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    feign:
    client:
    config:
    feignName:
    connectTimeout: 5000
    readTimeout: 5000
    loggerLevel: full
    errorDecoder: com.example.SimpleErrorDecoder
    retryer: com.example.SimpleRetryer
    requestInterceptors:
    - com.example.FooRequestInterceptor
    - com.example.BarRequestInterceptor
    decode404: false
    encoder: com.example.SimpleEncoder
    decoder: com.example.SimpleDecoder
    contract: com.example.SimpleContract
  2. 为 @FeignClient 指定默认配置
    也可以将自定义的配置指定为默认配置,以在 @EnableFeignClients 注释的属性 defaultConfiguration 中指定,此处指定的默认配置将对所有 Feign 客户端有效。
    如果更喜欢使用属性配置来配置所有 @FeignClient,则可以使用 feign name 为 default 来创建的配置属性。示例如下:
    application.yml

    1
    2
    3
    4
    5
    6
    7
    feign:
    client:
    config:
    default:
    connectTimeout: 5000
    readTimeout: 5000
    loggerLevel: basic

    如果同时创建了 @Configuration bean 和 在配置文件中使用了属性配置,则属性配置优先,会覆盖 @Configuration 的值。如果想改变 @Configuration 的优先级,可以改变 feign.client.default-to-properties 的值为 false

  3. 配置多个 Feign Client
    如果想要创建具有相同 nameurl 的多个 feign 客户端,以便它们指向同一服务器但每个都具有不同的自定义配置,必须使用 @FeClClient 注解的 contextId 属性以避免这些配置 Bean 的名称冲突。示例如下

    1
    2
    3
    4
    @FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
    public interface FooClient {
    //..
    }
    1
    2
    3
    4
    @FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
    public interface BarClient {
    //..
    }

备注:如果需要在 RequestInterceptor 中使用 ThreadLocal 绑定变量,则需要将 Hystrix 的线程隔离策略设置为 SEMAPHORE 或在 Feign 中禁用 Hystrix。如下设置:
application.yml

1
2
3
4
5
6
7
8
9
10
11
12
# To disable Hystrix in Feign
feign:
hystrix:
enabled: false

# To set thread isolation to SEMAPHORE
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE

手动创建 Feign Client

某些情况下,需要自定义 Feign Client。可以使用 Feign Builder API来创建。
下面示例,创建两个具有相同接口的 Feign Client,但使用单独的请求拦截器配置每个客户端。

1
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
//FeignClientsConfiguration.class 是 Spring Cloud Netflix 提供的默认配置
@Import(FeignClientsConfiguration.class)
class FooController {

private FooClient fooClient;

private FooClient adminClient;

//这里自动装配的 Contract bean 由 SpringMVC 注释提供
@Autowired
public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
//PROD-SVC 是客户端向其发送请求是服务名
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "http://PROD-SVC");

this.adminClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "http://PROD-SVC");
}
}

Feign Hystrix 支持

如果添加了 Hystrix 依赖包,并且 feign.hystrix.enabled=true,Feign 将所有方法包装在 断路器 中,返回一个可用的com.netflix.hystrix.HystrixCommand ,这允午使用响模式(通过调用 .toObservable().observe() 或异步使用(调用*.queue()*)。

如果要在某一个客户端的禁用 Hystrix 的支持,需要创建一个作用域是 prototype 范围的 Feign.Builder,@FeignClient 客户端 configuration 属性指定该配置类。例如:

1
2
3
4
5
6
7
8
@Configuration
public class FooConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}

备注:在 Spring Cloud Dalston 发布之前,如果Hystrix在类路径上,Feign会默认将所有方法包装在 断路器 中。 Spring Cloud Dalston 中更改了此默认行为,转而采用了选择加入方法。

Feign Hystrix 回退

Hystrix 支持回退的概念,即当断路器打开或执行出错时,会去执行一段默认的代码。

要为给定 @FeignClient 启用回退,冉要将 fallback 属性设置为实现了回退接口的类名,并将其声明为 Spring Bean。

1
2
3
4
5
6
7
8
9
10
11
12
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}

static class HystrixClientFallback implements HystrixClient {
@Override
public Hello iFailSometimes() {
return new Hello("fallback");
}
}

如果要访问触发回退的原因,需要使用 @FeignClient 的 fallbackFactory 属性,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}

@Component
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
@Override
public HystrixClient create(Throwable cause) {
return new HystrixClient() {
@Override
public Hello iFailSometimes() {
return new Hello("fallback; reason was: " + cause.getMessage());
}
};
}
}

备注: Feign 回退的实现以及 Hystrix 回退的工作方式存在局限性,对于返回 com.netflix.hystrix.hystrixcommandrx.observable 的方法,目前不支持回退。

Feign 和 @Primary

当 Feign 和 Hystrix 回退一起使用时,在应用上下文中会存在多个相同类型的 Bean,这将会导致 @Autowired 注解无法工具,因为没有哪一个 Bean 被确切地标记为 primary

为了解决这个问题,Spring Cloud Netflix 默认将所有 Feign 实例标记为 Primary(即 @FeignClient 注解的 primary 属性默认为 true),这样 Spring Framework 就会知道注入了那些 Bean。但在某此情况下,这种方式并不可取,可以关闭此默认行为。

1
2
3
4
@FeignClient(name = "hello", primary = false)
public interface HelloClient {
// methods here
}

Feign 继承支持

Feign 可以通过单个继承接来支持样板 API(抽出为公共接口),这样允许将公共操作基本的公共接口中。

UserService.java

1
2
3
4
5
public interface UserService {

@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
User getUser(@PathVariable("id") long id);
}

UserResource.java

1
2
3
4
@RestController
public class UserResource implements UserService {

}

UserClient.java

1
2
3
4
5
6
package project.user;

@FeignClient("users")
public interface UserClient extends UserService {

}

备注:通常不建议在服务器和客户端之间共享接口,这样会引入紧耦合,并且不能使用 Spring MVC(方法参数映射不会被继承)

Feign 请求/响应压缩

可以使用属性配置方式为 Feign 请求开启请求 / 响应 GZIP 压缩。

1
2
feign.compression.request.enabled=true
feign.compression.response.enabled=true

Feign 请求压缩为你提供类似于 Web 服务器中的设置:

1
2
3
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

通过这些属性,允许针对性选择要压缩的类型(media types)和指定最小压缩值的标准。

Feign 日志级别设置

每创建 Feign client 时会同时创建 logger。默认情况下,logger 的名称是用于创建 Feign Client 的接口的全限定类名。

Feign 日志仅响应 DEBUG 级别。即必须在配置文件中指定 Client 的日志级别为 DEBUG,自定义的 Logger.Level 才会生效。
application.yml.

1
2
3
logging.level.project.user.UserClient: DEBUG
//示例
logging.level.com.springcloud.sakilaconsumer.service.FeignInterface: DEBUG

可以为每个客户端配置 Logger.Level 日志级别:

  • NONE:无日志(默认值)。
  • BASIC:只输出请求方法和 URL 以及响应状态代码和执行时间。
  • HEADERS:输出基本信息及请求和响应头。
  • FULL:输出完整信息,包括请求和响应的头(headers)、体(body)、元数据(metadata)信息

如下示例,配置日志级别(Logger.Level) 为 FULL 级别,需要在 @FeignClient 注解的 configuration 属性指定该配置类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class FooConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

public static enum Level {
NONE,
BASIC,
HEADERS,
FULL;

private Level() {
}
}

Feign @QueryMap 支持

OpenFeign 原生自带的 @QueryMap 注解为将 pojo 映射为 get 参数提供支持,但缺少 value 属性,与 Spring 不兼容。为解决此问题,Spring Cloud OpenFeign 提供了相同作用的注解 @SpringQueryMap,注释 pojo 或 map 参数映射为请求参数。使用示例如下:

1
2
3
4
5
6
7
// Params.java
public class Params {
private String param1;
private String param2;

// [Getters and setters omitted for brevity]
}

Feign 客户端使用 @SpringQueryMap 注释 Params 类参数:

1
2
3
4
5
6
@FeignClient("demo")
public class DemoTemplate {

@GetMapping(path = "/demo")
String demoEndpoint(@SpringQueryMap Params params);
}

Feign 超时设置

通过 Request.Options 可以配置连接超时时间和读取超时时间。

1
2
3
4
5
6
7
8
9
10
@Configuration
public class FeignCustomConfig {

@Bean
Request.Options options(){
//第一个参数 connectTimeoutMillis 连接超时时间
//第二个参数 readTimeoutMillis 读取超时时间
return new Request.Options(5000, 10000);
}
}

原生 Feign 的使用

如果没有用到 Spring Cloud ,但是想用 Feign 来代替之前的接口调用方式,则需要使用 Feign 原生注解。

具体参考 OpenFeign 在 GitHub 上的使用说明。

Spring Cloud(五):声明式 REST 客户端 Feign

http://blog.gxitsky.com/2019/03/25/SpringCloud-05-rest-client-feign-1/

作者

光星

发布于

2019-03-25

更新于

2022-06-17

许可协议

评论