Spring Cloud OpenFeign Features
Declarative REST Client:Feign
Feign是一个声明式的web服务客户端,创建一个interface并添加注解就可以创建一个web服务客户端。提供了可插拔的注解包含Feign的注解和JAX-RS的注解,Feign同样支持可插拔的编码解码器。SpringCloud为Spring MVC的注解提供了支持并且默认使用同样的HttpMessageConverters。当使用Feign时,SpringCloud集成了Eureka,Spring Cloud CircuitBreaker以及Spring Cloud LoadBalancer来为Feign提供负载均衡方面的支持。
How to Include Feign
添加group为org.springframework.cloud,artifact id为spring-cloud-starter-openfeign的starter依赖。Spring Cloud Project page。查看Spring Cloud Project page以查看构建细节。
SpringBoot app 示例:
java
@SpringBootApplication
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
StoreClient.java
java
@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();
@RequestMapping(method = RequestMethod.GET, value = "/stores")
Page<Store> getStores(Pageable pageable);
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
@RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\d+}")
void delete(@PathVariable Long storeId);
}
在 @FeignClient 注解中,"stores"是用来创建一个 Spring Cloud LoadBalancer client。你也可以使用url参数制定一个URL。这个bean在application context的名称是这个interface的全限定类名。为了指定你自己的别名,你可以使用@FeignClient的qualifiers值。
load-balancer client会根据"stores"来找到物理的地址。如果你的应用是一个Eureka client,那么就会用过Eureka注册中心来解析"stores"。如果你不想使用Eureka,你可以通过SimpleDiscoveryClient在你的外部配置中定义一系列servers。
SpringCloud OpenFeign支持Spring Cloud LoadBanlancer的阻塞模式的所有Feature。更多信息请参考project documentation。
Tip
要在@Configuration上使用注解 @EnableFeignClients ,请确保指定client的位置,例如 @EnableFeignClients(basePackages = "com.example.clients")或者把他们详细列出来: @EnableFeignClients(clients = InventoryServiceFeignClient.class)
Attribute resolution mode
当创建Feign client bean的时候,我们处理通过@FeignClient注解传入的value。对于4.x,values会被立即处理,这对于大多数的使用场景都是好的解决方式,而且还支持AOT。
如果你希望这些参数被懒加载式处理,设置spring.cloud.openfeign.lazy-attributes-resolution=true。
Overriding Feign Default
SpringCloud feign的一个核心概念就是具名客户端,每个feign client都作为一组组件的一部分来发送请求,并且这一组组件的名字由@FeignClient注解所给。Spring Cloud会使用FeignClientsConfiguration来创建出一组组件作为ApplicationContext,其中包含一个feign.Decoder,一个feign.Encoder和一个feign.Contract。可以使用@FeignClient注解的contextId参数来覆盖这一组组件的名字。
SpringCloud可以让你完全控制feign客户端,通过声明额外的配置(其在FeignClientsConfiguration之上),如下所示
java
@FeignClient(name = "stores", configuration = FooConfiguration.class)
public interface StoreClient {
//..
}
在这个事例中 FooConfiguration的配置会覆盖FeignClientsConfiguration的配置。
Note
FooConfiguration不需要被@Configuration注解。然而,如果被@Configuration注解了,要注意不要被@ComponentScan扫描到,否则它将会作为feign.Decoder, feign.Encoder, feign.Contract等,默认的配置。你可以将他们放在与@ComponentScan 或@SpringBootApplication分离的包下,或者显式地从@ComponentScan排除掉。
Note
使用@FeignClient注解的contextId参数除了改变ApplicationContext的名字外,还会覆盖client的别名,还会作为client所创建的configuration bean名称的一部分。
Warning
之前,使用url参数时,不必要有name参数,但是现在是必要的。
name和url参数是支持占位符的
java
@FeignClient(name = "${feign.name}", url = "${feign.url}")
public interface StoreClient {
//..
}
SpringCloud OpenFeign默认提供了系列的bean (BeanType beanName: ClassName)
DecoderfeignDecoder:ResponseEntityDecoder(which wraps aSpringDecoder)EncoderfeignEncoder:SpringEncoderLoggerfeignLogger:Slf4jLoggerMicrometerObservationCapabilitymicrometerObservationCapability: Iffeign-micrometeris on the classpath andObservationRegistryis availableMicrometerCapabilitymicrometerCapability: Iffeign-micrometeris on the classpath,MeterRegistryis available andObservationRegistryis not availableCachingCapabilitycachingCapability: If@EnableCachingannotation is used. Can be disabled viaspring.cloud.openfeign.cache.enabled.ContractfeignContract:SpringMvcContractFeign.BuilderfeignBuilder:FeignCircuitBreaker.BuilderClientfeignClient: 如果SpringCloud LoadBanlancer在classpath上,会使用FeignBlockingLoadBalancerClient,如果不在classpath上,就会使用默认的feign client。
Note
spring-cloud-starter-openfeign支持 spring-cloud-starter-loadbalancer,然而,作为一个可选的依赖,如果想使用它请确保该依赖在你的classpath下。
OkHttpClient, Apache HttpClient 5 和Http2Client, Feign clients可以通过设置相应配置并添加对应依赖来启用相应HttpClient, spring.cloud.openfeign.okhttp.enabled 或spring.cloud.openfeign.httpclient.hc5.enabled 或spring.cloud.openfeign.http2client.enabled 。
你可以进一步通过配置项来进一步对http clients进行自定义。例如spring.cloud.openfeign.httpclient.xxx以httpclient为前缀的配置项将会应用于所有客户端,以httpclient.hc5为前缀的将应用于Apache HttpClient5。以httpclient.okhttp为前缀的将应用于OkHttpClient 。以httpclient.http2为前缀的将应用于Http2Client。可以在附录中查询完整的配置列表。对于HttpClient 5 ,你还可以实现HttpClientBuilderCustomizer接口来实现编码式配置。
Tip
从Spring Cloud OpenFeign4 开始,Feign Apache HttpClient 4 不再被支持。所以我们建议以使用Apache HttpClient 5。
Spring Cloud OpenFeign默认并没有提供下面的bean,但是在创建feign client的时候会在application context中查找这些类型的bean。
Logger.LevelRetryerErrorDecoderRequest.OptionsCollection<RequestInterceptor>SetterFactoryQueryMapEncoderCapability(MicrometerObservationCapabilityandCachingCapabilityare provided by default)
Retryer 类型的 Retryer.NEVER_RETRY bean会被默认创建,它会禁用掉重试。需要注意的是,这种重试行为与Feign默认行为是不一样的。默认的将会自动重试IOExceptions,将它们看做短暂的网络相关的异常,还会重试那些从ErrorDecoder中抛出的任何RetryableException 。
创建这些类型的bean,并将它们放置在@FeignClient配置中(像上面的 FooConfiguration 一样)。这样你就可以覆盖每一个上述的bean。例如
java
@Configuration
public class FooConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
}
这将会用 feign.Contract.Default代替SpringMvcContract,并添加一个RequestInterceptor到RequestInterceptor的集合中。
@FeignClient还可以使用configuration properties进行配置
yml
spring:
cloud:
openfeign:
client:
config:
feignName:
url: http://remote-service.com
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
defaultQueryParameters:
query: queryValue
defaultRequestHeaders:
header: headerValue
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
responseInterceptor: com.example.BazResponseInterceptor
dismiss404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
capabilities:
- com.example.FooCapability
- com.example.BarCapability
queryMapEncoder: com.example.SimpleQueryMapEncoder
micrometer.enabled: false
例子中的feignName指的是 @FeignClient 的value,也可以是@FeignClient的name,@FeignClient 的contextId。在负载均衡场景下,它相当于server app的serviceId,用于拿取对应的实例。在其中所配置的哪些类必须要在Spring Context中有一个bean或者有一个默认的构造函数。
可以在@EnableFeignClients注解的defaultConfiguration参数中指定默认的配置,它会对所有的feign client生效。
如果你倾向于使用配置项来配置所有的@FeignClient,那么你可以以default作为feign 名称进行配置。
你可以使用spring.cloud.openfeign.client.config.feignName.defaultQueryParameters和spring.cloud.openfeign.client.config.feignName.defaultRequestHeaders来指定名为feignName的client每次请求所携带的query paramters和headers。
application.yml
yml
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
如果同时创建了 @Configuration bean和配置文件,那么会使用配置文件进行配置。但是你如果想改成 @Configuration优先的形式,你可以设置 spring.cloud.openfeign.client.default-to-properties=false。
如果你想创建具有相同name或url的feign client,让他们指向同一server,但是又具有不同的自定义配置,你可以使用@FeignClient的contextId参数来避免这些configuration bean的冲突。
java
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
public interface FooClient {
//..
}
java
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
public interface BarClient {
//..
}
你可以配置FeignClient 不从父Context中继承beans。你可以覆盖FeignClientConfigurerbean中的inheritParentConfiguration()为false。
java
@Configuration
public class CustomConfiguration {
@Bean
public FeignClientConfigurer feignClientConfigurer() {
return new FeignClientConfigurer() {
@Override
public boolean inheritParentConfiguration() {
return false;
}
};
}
}
Tip
默认情况下,Feign client不会对/进行编码,你可以通过设置spring.cloud.openfeign.client.decodeSlash=false来改变该行为。
SpringEncoder configuration
在我们提供的SpringEncoder中,我们为二进制内容设置了空字符集,对于其他设置了UTF-8字符集。
Timeout Handling
我们可以为默认和named client上配置超时时间。OpenFeign可以使用两种超时参数
connectTimeout防止由于服务器处理时间过长而阻塞调用者readTimeout是从连接建立之后,返回response时间过长是被触发
Creating Feign Clients Manually
你可以通过Feign Builder API来创建Client。下面示例展示了使用相同接口却配置了不同请求拦截器的Feign Client。
java
@Import(FeignClientsConfiguration.class)
class FooController {
private FooClient fooClient;
private FooClient adminClient;
@Autowired
public FooController(Client client, Encoder encoder, Decoder decoder, Contract contract, MicrometerObservationCapability micrometerObservationCapability) {
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.addCapability(micrometerObservationCapability)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "https://PROD-SVC");
this.adminClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.addCapability(micrometerObservationCapability)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "https://PROD-SVC");
}
}
Note
在上面示例中,FeignClientsConfiguration.class是由 Spring Cloud OpenFeign提供的默认配置。
Note
PROD-SVC是client所请求服务的name
Note
Feign的 Contract 对象定义了什么注解是在接口中可用的。autowired Contractbean为SpringMVC注解提供了支持而不是使用Feign原生的注解。
你同样可以使用Builder来配置FeignClient不从父Context中继承beans,通过调用 inheritParentContext(false)。
Feign Spring Cloud CircuitBreaker Support(对熔断器的支持)
如果Spring Cloud CircuitBreaker 在classpath中,并且设置了spring.cloud.openfeign.circuitbreaker.enabled=true,那么Feign会为所有的方法用熔断器包裹。
想要在每个client上禁用 Spring Cloud CircuitBreaker,可以创建一个普通的Feign.Builder,设置其为prototype
java
@Configuration
public class FooConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}
熔断器的命名遵循的规则是<feignClientClassName>#<calledMethod>(<parameterTypes>)。当调用FooClient中的为bar的方法且无参数时,那这个熔断器会命名为FooClient#bar()。
Note
从2020.0.2开始,熔断器的名称就从<feignClientName>_<calledMethod>改变了。根据2020.0.4的介绍,可以使用CircuitBreakerNameResolver自定义命名模式。
java
@Configuration
public class FooConfiguration {
@Bean
public CircuitBreakerNameResolver circuitBreakerNameResolver() {
return (String feignClientName, Target<?> target, Method method) -> feignClientName + "_" + method.getName();
}
}
为了启用SpringCloud CircuitBreaker group,设置spring.cloud.openfeign.circuitbreaker.group.enabled=true。
Configuring CircuitBreakers With Configuration Properties
你可以通过configuration properties来对CircuitBreakers 进行配置
例如,你有个Feign client定义如下
java
@FeignClient(url = "http://localhost:8080")
public interface DemoClient {
@GetMapping("demo")
String getDemo();
}
你可以对其使用configuration properties配置
yaml
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
alphanumeric-ids:
enabled: true
resilience4j:
circuitbreaker:
instances:
DemoClientgetDemo:
minimumNumberOfCalls: 69
timelimiter:
instances:
DemoClientgetDemo:
timeoutDuration: 10s
Note
如果你想要回到Spring Cloud 2022.0.0之前熔断器命名的方式,你可以设置spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled=true
Feign Spring Cloud CircuitBreaker Fallbacks
Spring Cloud CircuitBreaker 支持fallback的模式,即当连接不通或者发生错误时,会执行特定的代码。为了对@FeignClient启用fallback,可以添加实现fallback的类,并将类名设置到fallback参数中。需要注意实现fallback的类应注册到Spring中作为bean。
java
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
protected interface TestClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello();
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
String getException();
}
@Component
static class Fallback implements TestClient {
@Override
public Hello getHello() {
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
}
@Override
public String getException() {
return "Fixed response";
}
}
如果想要知道是什么具体原因触发了fallback,可以使用 @FeignClient的 fallbackFactory 参数。
java
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
fallbackFactory = TestFallbackFactory.class)
protected interface TestClientWithFactory {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello();
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
String getException();
}
@Component
static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> {
@Override
public FallbackWithFactory create(Throwable cause) {
return new FallbackWithFactory();
}
}
static class FallbackWithFactory implements TestClientWithFactory {
@Override
public Hello getHello() {
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
}
@Override
public String getException() {
return "Fixed response";
}
}
Feign and @Primary
当在Feign中使用Spring Cloud CircuitBreaker fallback时,在ApplicationContext中对于同一类型会有多个bean。这会导致@Autowired工作不正常。因此SpringCloud OpenFeign会把所有Feign示例标记为@Primary,这样Spring框架就知道应该注入哪个bean。如果你不需要该特性,可以将@FeignClient的primary参数设置为false。
java
@FeignClient(name = "hello", primary = false)
public interface HelloClient {
// methods here
}
Feign Inheritance Support
Feign支持通过继承interface来实现模板api的功能,这样就可以方便地将常用的操作分别放在一些base interface中。
java
public interface UserService {
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
User getUser(@PathVariable("id") long id);
}
java
@RestController
public class UserResource implements UserService {
}
java
@FeignClient("users")
public interface UserClient extends UserService {
}
Warning
@FeignClientinterfaces不应该在server和client之间共享,并且在@FeignClient在类层面用@RequestMapping注解是不被支持的。
Feign request/response compression
你也许想对你的Feign请求响应开启GZIP压缩功能,可以进行如下配置
properties
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.response.enabled=true
Feign请求的压缩设置与对web server的设置很相似
properties
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json
spring.cloud.openfeign.compression.request.min-request-size=2048
这些配置让你可以修改压缩的media type和最小请求大小阈值。
Feign logging
对于每一个FeignClient,都会有一个相应的logger,logger的名称是创建feign client的interface的全类名。Feign logging只对应DEBUGlevel。
properties
logging.level.project.user.UserClient: DEBUG
Logger.Level对象用来告诉Feign怎么去log
NONE, 不打印日志(默认)BASIC, 请求方法和URL,以及响应status code和执行时间HEADERS, 打印basis日志,还有请求和响应headers。FULL, 打印请求和响应的headers,body以及元数据
例如,如下设置Logger.Level为FULL:
java
@Configuration
public class FooConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}