Feign 是一个声明式 Web 客户端,让 Rest 服务调用更简单。要使用 Feign ,只需在编写接口并添加其注解,就可以定义好 http 请求的参数、格式、地址等信息。
Feign 会完全代理 Http 请求,只需向调用方法一样调用 feign 注解的客户端就可以完成服务请求及相关处理。
Spring Cloud 支持集成 Ribbon 和 Eureka,Feign 和他们一起使用时支持客户端负载均衡功能。Spring Cloud OpenFeign 文档。
Java 项目接口调用
Java 项目中调用接口通常会用到以下工具:
- HttpClient:HttpClient 是 Apache 下的子项目,功能丰富,易用,灵活和高效。
- Okhttp:一个处理网络请求的开源框架。Okhttp 具有更简洁的 API,高效的性能,并支持多种协议。
- Httpurlconnection:是 Java 的标准类,继承自 URLConnection,发送 get/post 请求,使用比较复杂。
- RestTemplate:是 Spring 提供的用于访问 Rest 服务的客户端,便捷高效。
Spring Cloud Feign
Spring Cloud 架构项目,在客户端(消费者)应用引入 eureka-client、ribbon、feign 依赖;在 Service 层创建接口文件,接口上使用注解 @FeignClient 指定要调用远程服务的应用名,创建接口方法,根据远程服务接口的调用规则定义此接口(类似于 Controller 层方法的注解定义请求路径、请求类型、参数类型等)
引入 Feign 依赖
<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 注解使用
启用 Feign 客户端:Spring Boot 项目在入口类 Application 上添加启用 Feign 客户端的注解 @EnableFeignClients
@SpringBootApplication @EnableFeignClients public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
使用 Feign 客户端:在 Service 层创建接口,在接口上使用 Feign 客户端注解 @FeignClient
//注解属性还可使用 ${} 方式注入环境变量中配置的值 //如:@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 的别名。
消费者端的 Controller 层注入 @FeignClient注解的 Service 接口,在 Controller 层调用 Service 层接口。
Feign 调用简单示例
消费者应用:Controller 层
@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 层接口
@FeignClient(name = "sakila-service1",path = "/service") public interface FeignInterface { @GetMapping("/home") public String callHome(); }
服务提供者:远程服务接口
@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; }
FeignClient 重名处理
若在一个服务里定义了多个 name 属性相同的 FeignClient ,直接启动是会报错。如下:
@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 属性存在重复。
三种解决方案:
配置多个相同 name 的 Feign Client,使用
@FeignClient
注解的contextId
属性以避免这些配置 Bean 的名称冲突。-- 推荐这种同一个服务的调用,写在同一个 @FeignClient 客户端中,这样 name 就不会重复。
开启 Spring Bean 允许覆盖,默认是禁止的,如下:
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 客户端。如下:
@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 属性。
name 和 url 属性还支持占位符注入环境变量中的值,如下:
@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。
自定义 Feign 契约配置 Bean
原生 Feign 不支持 Spring MVC 注解。Spring Cloud OpenFeign 在 Feign 基础上做了扩展,可以让 Feign 支持 Spring MVC 的注解(如 @RequestMapping)来调用。
若想在 Spring Cloud 中使用原生的注解方式来定义客户端,可通过修改 Contract 这个配置,Spring Cloud 中默认是 SpringMvcContract。@Configuration public class FooConfiguration { @Bean public Contract feignContract() { //替换默认的 SpringMvcContract return new feign.Contract.Default(); } }
当使用默认的 Contract 后,Feign 客户端的接口方法就不能使用 Spring MVC 的注解了。
自定义 Basic 安全认证 Bean
通常调用的接口都是有权限控制的,认证信息大多通过参数传递,或通过请求头传递,如 Basic 认证方式。在 Feign 中可直接配置 Basic 认证方式。@Configuration public class FooConfiguration { @Bean public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { return new BasicAuthRequestInterceptor("user", "password"); } }
也可以自定义属于自己的认证方式,其实就是自定义一个拦截器,实现 RequestInterceptor 接口,在 apply() 方法中执行请求之前的认证操作,然后往请求头中设置认证之后的信息。然后在配置类中声明自定义的认证拦截器为 Bean。
Feign 属性配置
@FeignClient 也可以合用属性配置进行配置
application.ymlfeign: 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
为 @FeignClient 指定默认配置
也可以将自定义的配置指定为默认配置,以在 @EnableFeignClients 注释的属性 defaultConfiguration 中指定,此处指定的默认配置将对所有 Feign 客户端有效。
如果更喜欢使用属性配置来配置所有 @FeignClient,则可以使用 feign name 为 default 来创建的配置属性。示例如下:
application.ymlfeign: client: config: default: connectTimeout: 5000 readTimeout: 5000 loggerLevel: basic
如果同时创建了 @Configuration bean 和 在配置文件中使用了属性配置,则属性配置优先,会覆盖 @Configuration 的值。如果想改变 @Configuration 的优先级,可以改变 feign.client.default-to-properties 的值为 false。
配置多个 Feign Client
如果想要创建具有相同 name 或 url 的多个 feign 客户端,以便它们指向同一服务器但每个都具有不同的自定义配置,必须使用 @FeClClient 注解的 contextId 属性以避免这些配置 Bean 的名称冲突。示例如下@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class) public interface FooClient { //.. }
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class) public interface BarClient { //.. }
备注:如果需要在 RequestInterceptor 中使用 ThreadLocal 绑定变量,则需要将 Hystrix 的线程隔离策略设置为 SEMAPHORE 或在 Feign 中禁用 Hystrix。如下设置:
application.yml
# 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,但使用单独的请求拦截器配置每个客户端。
//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 属性指定该配置类。例如:
@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。
@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 属性,示例如下:
@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.hystrixcommand 和rx.observable 的方法,目前不支持回退。
Feign 和 @Primary
当 Feign 和 Hystrix 回退一起使用时,在应用上下文中会存在多个相同类型的 Bean,这将会导致 @Autowired 注解无法工具,因为没有哪一个 Bean 被确切地标记为 primary。
为了解决这个问题,Spring Cloud Netflix 默认将所有 Feign 实例标记为 Primary(即 @FeignClient 注解的 primary 属性默认为 true),这样 Spring Framework 就会知道注入了那些 Bean。但在某此情况下,这种方式并不可取,可以关闭此默认行为。
@FeignClient(name = "hello", primary = false)
public interface HelloClient {
// methods here
}
Feign 继承支持
Feign 可以通过单个继承接来支持样板 API(抽出为公共接口),这样允许将公共操作基本的公共接口中。
UserService.java
public interface UserService {
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
User getUser(@PathVariable("id") long id);
}
UserResource.java
@RestController
public class UserResource implements UserService {
}
UserClient.java
package project.user;
@FeignClient("users")
public interface UserClient extends UserService {
}
备注:通常不建议在服务器和客户端之间共享接口,这样会引入紧耦合,并且不能使用 Spring MVC(方法参数映射不会被继承)
Feign 请求/响应压缩
可以使用属性配置方式为 Feign 请求开启请求 / 响应 GZIP 压缩。
feign.compression.request.enabled=true
feign.compression.response.enabled=true
Feign 请求压缩为你提供类似于 Web 服务器中的设置:
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.
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 属性指定该配置类。
@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 参数映射为请求参数。使用示例如下:
// Params.java
public class Params {
private String param1;
private String param2;
// [Getters and setters omitted for brevity]
}
Feign 客户端使用 @SpringQueryMap 注释 Params 类参数:
@FeignClient("demo")
public class DemoTemplate {
@GetMapping(path = "/demo")
String demoEndpoint(@SpringQueryMap Params params);
}
Feign 超时设置
通过 Request.Options 可以配置连接超时时间和读取超时时间。
@Configuration
public class FeignCustomConfig {
@Bean
Request.Options options(){
//第一个参数 connectTimeoutMillis 连接超时时间
//第二个参数 readTimeoutMillis 读取超时时间
return new Request.Options(5000, 10000);
}
}
原生 Feign 的使用
如果没有用到 Spring Cloud ,但是想用 Feign 来代替之前的接口调用方式,则需要使用 Feign 原生注解。
具体参考 OpenFeign 在 GitHub 上的使用说明。
注意:本文归作者所有,未经作者允许,不得转载