17. 声明 REST Client:Feign

假装是一个声明性的 web service client。这使得编写 web service clients 更容易。要使用 Feign,请创建一个界面并对其进行注释。它具有可插入的 annotation 支持,包括 Feign annotations 和 JAX-RS _notnotations。 Feign 还支持可插拔编码器和解码器。 Spring Cloud 添加了对 Spring MVC annotations 的支持,并使用了 Spring Web 中默认使用的相同HttpMessageConverters。使用 Feign 时,Spring Cloud 集成 Ribbon 和 Eureka 以提供负载均衡的 http client。

17.1 如何包含 Feign

要在项目中包含 Feign,请使用带有 group org.springframework.cloud和 artifact id spring-cloud-starter-openfeign的 starter。有关使用当前 Spring Cloud Release Train 设置 build 系统的详细信息,请参阅Spring Cloud 项目页面

Example spring boot app

@Configuration
@ComponentScan
@EnableAutoConfiguration
@EnableFeignClients
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

StoreClient.java.

@FeignClient("stores")
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 annotation 中,String value(上面的“stores”)是一个任意 client name,用于创建 Ribbon 负载均衡器(请参阅以下是 Ribbon 支持的详细信息)。您还可以使用url属性指定 URL(绝对 value 或仅指定主机名)。 application context 中 bean 的 name 是接口的完全限定 name。要指定自己的别名 value,可以使用@FeignClient annotation 的qualifier value。

上面的 Ribbon client 将要发现“stores”服务的物理地址。如果您的 application 是 Eureka client,那么它将解析 Eureka 服务注册表中的服务。如果您不想使用 Eureka,只需在外部 configuration 中配置服务器列表(请参阅以上为 example)。

17.2 覆盖 Feign 默认值

Spring Cloud 的 Feign 支持的核心概念是命名的 client。每个 feign client 都是一个组件集合的一部分,它们一起工作以按需联系 remote 服务器,并且整体有一个 name,您可以使用@FeignClient annotation 将其作为 application 开发人员提供。 Spring Cloud 使用FeignClientsConfiguration为每个命名的 client 创建一个新的集合作为ApplicationContext。这包含(除其他外)feign.Decoderfeign.Encoderfeign.Contract

Spring Cloud 允许您通过使用@FeignClient声明其他 configuration(在FeignClientsConfiguration之上)来完全控制 feign client。 例:

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

在这种情况下,client 由FeignClientsConfiguration中的组件和FooConfiguration中的任何组件组成(后者将覆盖前者)。

FooConfiguration不需要用@Configuration注释。但是,如果是,则注意将其从任何包含此 configuration 的@ComponentScan中排除,因为它将在指定时成为feign.Decoderfeign.Encoderfeign.Contract等的默认源。这可以通过将其放在任何@ComponentScan@SpringBootApplication的单独的 non-overlapping 包中来避免,或者可以在@ComponentScan中明确排除。

现在不推荐使用serviceId属性,而是使用name属性。

以前,使用url属性不需要name属性。现在需要使用name

nameurl属性支持占位符。

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

Spring Cloud Netflix 默认为 feign(BeanType beanName:ClassName)提供以下 beans:

  • Decoder feignDecoder:ResponseEntityDecoder(包装SpringDecoder)

  • Encoder feignEncoder:SpringEncoder

  • Logger feignLogger:Slf4jLogger

  • Contract feignContract:SpringMvcContract

  • Feign.Builder feignBuilder:HystrixFeign.Builder

  • Client feignClient:如果启用了 Ribbon,则为LoadBalancerFeignClient,否则使用默认的 feign client。

可以通过分别将feign.okhttp.enabledfeign.httpclient.enabled设置为true并将它们放在 classpath 上来使用 OkHttpClient 和 ApacheHttpClient feign clients。您可以通过使用 Apache 提供或使用 OK HTTP OkHttpClient whe 来自定义 HTTP client。

Spring Cloud Netflix 默认情况下不为 feign 提供以下 beans,但仍然会从 application context 中查找这些类型的 beans 以创建 feign client:

  • Logger.Level

  • Retryer

  • ErrorDecoder

  • Request.Options

  • Collection<RequestInterceptor>

  • SetterFactory

创建其中一种类型的 bean 并将其置于@FeignClient configuration(例如上面的FooConfiguration)中,可以覆盖所描述的每个 beans。 例:

@Configuration
public class FooConfiguration {
    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("user", "password");
    }
}

这将SpringMvcContract替换为feign.Contract.Default并将RequestInterceptor添加到RequestInterceptor的集合中。

也可以使用 configuration properties 配置@FeignClient

application.yml

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

可以以与上述类似的方式在@EnableFeignClients属性defaultConfiguration中指定默认配置。不同之处在于此 configuration 将适用于所有 feign clients。

如果您更喜欢使用 configuration properties 配置所有@FeignClient,则可以使用default feign name 创建 configuration properties。

application.yml

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

如果我们同时创建@Configuration bean 和 configuration properties,configuration properties 将获胜。它将覆盖@Configuration值。但是如果要将优先级更改为@Configuration,则可以将feign.client.default-to-properties更改为false

如果需要在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

17.3 手动创建 Feign Clients

在某些情况下,可能需要以使用上述方法无法实现的方式自定义 Feign Clients。在这种情况下,您可以使用Feign Builder API创建 Clients。下面是一个 example,它创建了两个具有相同接口的 Feign Clients,但每个都配置了一个单独的请求拦截器。

@Import(FeignClientsConfiguration.class)
class FooController {

	private FooClient fooClient;

	private FooClient adminClient;

    	@Autowired
	public FooController(
			Decoder decoder, Encoder encoder, Client client) {
		this.fooClient = Feign.builder().client(client)
				.encoder(encoder)
				.decoder(decoder)
				.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
				.target(FooClient.class, "http://PROD-SVC");
		this.adminClient = Feign.builder().client(client)
				.encoder(encoder)
				.decoder(decoder)
				.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
				.target(FooClient.class, "http://PROD-SVC");
    }
}

在上面的例子中FeignClientsConfiguration.class是 Spring Cloud Netflix 提供的默认 configuration。

PROD-SVC是 Clients 将向其发出请求的服务的 name。

17.4 Feign Hystrix 支持

如果 Hystrix 位于 classpath 和feign.hystrix.enabled=true上, Feign 将使用断路器包装所有方法。也可以返回com.netflix.hystrix.HystrixCommand。这允许您使用 reactive 模式(通过调用.toObservable().observe()或异步使用(通过调用.queue())。

要在 per-client 基础上禁用 Hystrix 支持,请使用“prototype”范围创建一个带有 e.g 的 vanilla Feign.Builder。:

@Configuration
public class FooConfiguration {
    	@Bean
	@Scope("prototype")
	public Feign.Builder feignBuilder() {
		return Feign.builder();
	}
}

在 Spring Cloud Dalston 发布之前,如果 Hystrix 在 classpath 上_Fign 将默认包装断路器中的所有方法。在 Spring Cloud Dalston 中更改了此默认行为,支持 opt-in 方法。

17.5 Feign Hystrix 后退

Hystrix 支持回退的概念:当电路打开或出现错误时执行的默认 code 路径。要为给定的@FeignClient启用回退,请将fallback属性设置为实现回退的 class name。您还需要将 implementation 声明为 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 中_backbacks 的实现以及 Hystrix 回退的工作方式存在限制。 return com.netflix.hystrix.HystrixCommandrx.Observable的方法目前不支持回退。

17.6 Feign 和 @Primary

将 Feign 与 Hystrix 回退一起使用时,同一类型的ApplicationContext中有多个 beans。这将导致@Autowired不起作用,因为没有一个 bean 或一个标记为主要的 bean。要解决此问题,Spring Cloud Netflix 将所有 Feign 实例标记为@Primary,因此 Spring Framework 将知道 inject 为 inject。在某些情况下,这可能并不理想。要关闭此行为,请将@FeignClientprimary属性设置为 false。

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

17.7 Feign 继承支持

Feign 通过 single-inheritance 接口支持样板 api。这允许将 common 操作分组到方便的基接口。

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 {

}

通常不建议在服务器和 client 之间共享接口。它引入了紧耦合,实际上也不能与当前形式的 Spring MVC 一起使用(方法参数映射不会被继承)。

17.8 Feign request/response 压缩

您可以考虑为 Feign 请求启用请求或响应 GZIP 压缩。您可以通过启用其中一个 properties 来执行此操作:

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

这些 properties 允许您选择压缩媒体类型和最小请求阈值长度。

17.9 Feign logging

为每个创建的 Feign client 创建一个 logger。默认情况下,logger 的 name 是用于创建 Feign client 的接口的完整 class name。 Feign logging 仅响应DEBUG level。

application.yml.

logging.level.project.user.UserClient: DEBUG

您可以根据 client 配置Logger.Level object,告诉 Feign 要 log 多少。选择是:

  • NONE,没有 logging(DEFAULT)。

  • BASIC,Log 只有请求方法和 URL 以及响应状态 code 和执行 time。

  • HEADERS,记录基本信息以及请求和响应 headers。

  • FULL,记录请求和响应的 headers,body 和元数据。

对于 example,以下将Logger.Level设置为FULL

@Configuration
public class FooConfiguration {
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}