Spring Cloud 声明式的 Rest 客户端 Feign

Feign是一个声明式的Rest客户端,它可以跟SpringMVC的相关注解一起使用,也可以使用Spring Web的HttpMessageConverter进行请求或响应内容的编解码。其底层使用的Ribbon和Eureka,从而拥有客户端负载均衡的功能。使用它需要在pom.xml中加入spring-cloud-starter-openfeign依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

需要使用Eureka的服务发现功能,则还需加入spring-cloud-starter-netflix-eureka-client依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

如果希望在声明客户端的时候还能使用Spring Web的相关注解,比如@RequestMapping,则可以添加spring-boot-starter-web依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

然后需要在配置类上使用@EnableFeignClients启用Feign客户端支持。

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

需要使用Eureka的服务发现功能时,还需要在application.properties中定义Eureka的相关信息。

eureka.client.registerWithEureka=false
eureka.client.serviceUrl.defaultZone=http://localhost:8089/eureka/

不使用Eureka的服务发现功能时,则可以通过<serviceId>.ribbon.listOfServers指定服务对应的服务器地址。这是属于Ribbon的功能,更多相关信息可以参考前面介绍过的Ribbon相关内容。

现假设有一个服务hello,其有两个实例,分别对应localhost:8080localhost:8081。假设hello服务有一个服务地址是/abc,GET请求,即分别可以通过http://localhost:8080/abchttp://localhost:8081访问到hello服务的/abc。现在我们的客户端需要通过Feign来访问服务hello的/abc。我们可以定义如下这样一个接口,在接口上定义@FeignClient("hello"),声明它是一个Feign Client,名称是hello(使用服务发现时对应的serviceId也是hello),在该接口里面定义的helloWorld()上使用了Spring Web的@GetMapping("abc")声明了对应的Http地址是/abc。Spring会自动扫描@FeignClient,并把HelloService初始化为bean,我们可以在需要使用服务hello的地方注入HelloService,当访问HelloService的helloWorld()时,将转而请求服务hello的/abc,即将访问http://localhost:8080/abchttp://localhost:8081/abc

@FeignClient("hello")
public interface HelloService {
    @GetMapping("abc")
    String helloWorld();
  
}

使用的时候就把HelloService当做一个普通的bean进行注入,然后调用其对应的接口方法,比如下面这样。

@RestController
@RequestMapping("hello")
public class HelloController {
    @Autowired
    private HelloService helloService;
  
    @GetMapping
    public String helloWorld() {
        return this.helloService.helloWorld();
    }
}

声明Feign Client的映射路径时也可以使用其它Spring Web的注解,比如@PostMapping@DeleteMapping@PathVariable@RequestBody等。

@GetMapping("path_variable/{pathVariable}")
String pathVariable(@PathVariable("pathVariable") String pathVariable);
@PostMapping("request_body")
String requestBody(@RequestBody Map<String, Object> body);

在定义Feign Client的名称时也可以使用Placeholder,比如@FeignClient("${feign.client.hello}"),此时对应的Feign Client的名称可以在application.properties中通过feign.client.hello属性指定。

直接指定服务端URL

@FeignClient也支持直接指定服务端的URL,此时便不会再通过服务发现组件去取服务地址了。比如下面代码中我们通过@FeignClient的url属性指定了服务地址是http://localhost:8901,那么当我们调用其helloWorld()时将向http://localhost:8901/abc发起请求,而不会再向服务发现组件获取名为hello的服务对应的服务地址了。

@FeignClient(name="hello", url = "http://localhost:8901")
public interface HelloService {
    @GetMapping("abc")
    String helloWorld();
  
}

url属性对应的访问协议是可以忽略的,所以上面的配置也可以写成@FeignClient(name="hello", url = "localhost:8901")

默认配置

Spring Cloud Feign默认会由org.springframework.cloud.openfeign.FeignClientsConfiguration创建一系列的bean,比如feign.codec.Decoderfeign.codec.Encoder等。FeignClientsConfiguration在定义这些bean时基本都定义了@ConditionalOnMissingBean,如果有需要,则定义自己的对应类型的bean可以直接替换默认的实现。比如下面代码中定义了一个@Configuration类,其中定义了一个feign.codec.Decoder,该Decoder会把所有响应内容都当做String处理,且在前面附加上一段文字。该Decoder将对所有的Feign Client生效。

@Configuration
public class DefaultConfiguration {
    @Bean
    public Decoder decoder() {
        return new DefaultDecoder();
    }
  
    public static class DefaultDecoder implements Decoder {
        @Override
        public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
            return "from default decode : " + Util.toString(response.body().asReader());
        }
    
    }
  
}

如果只希望对某个Feign Client进行特殊配置,则可以在@FeignClient上通过configuration属性指定特定的配置类。

@FeignClient(name="${feign.client.hello}", configuration=HelloFeignConfiguration.class)
public interface HelloService {
    @GetMapping("hello")
    String helloWorld();
  
}

然后在对应的配置类中定义特定的配置bean。下面的代码中我们也是定义了一个feign.codec.Decoder,其在相应内容前加了一句简单的话,它只对上面配置的feign.client.hello生效。

@Slf4j
public class HelloFeignConfiguration {
    @Bean
    public Decoder decoder() {
        return new HelloDecoder();
    }
  
    public static class HelloDecoder implements Decoder {
        @Override
        public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
            log.info("receive message, type is {}", type);
            return "from hello decoder : " + Util.toString(response.body().asReader());
        }
    
    }
  
}

在上面的HelloFeignConfiguration类中,我们没有标注@Configuration,特定Feign Client使用的配置信息可以不加配置类上加@Configuration,也不建议加@Configuration。因为加了@Configuration,而其又在默认的bean扫描路径下,则其中的bean定义都会生效,则其将变为所有的Feign Client都共享的配置。 当同时存在默认的Feign Client配置和特定的Feign Client的配置时,特定的Feign Client的配置将拥有更高的优先级,即特定的Feign Client的配置将覆盖默认的Feign Client的配置。但是如果在特定的Feign Client中没有定义的配置,则仍将以默认的Feign Client中配置的为准。

org.springframework.cloud.openfeign.FeignAutoConfiguration中也会创建一些bean,比如feign.Client,默认会使用org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient,其底层会使用JDK的URLConnection进行Http交互。如果需要使用基于Apache Http Client的实现需要ClassPath下存在feign.httpclient.ApacheHttpClient,此时将由org.springframework.cloud.openfeign.ribbon.HttpClientFeignLoadBalancedConfiguration创建LoadBalancerFeignClient类型的bean,其底层使用基于Apache Http Client实现的ApacheHttpClient。在pom.xml中添加如下依赖可以引入feign.httpclient.ApacheHttpClient

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

底层使用ApacheHttpClient时,如果bean容器中存在org.apache.http.impl.client.CloseableHttpClient类型的bean,则将使用该bean,否则将创建一个默认的CloseableHttpClient

除了通过代码进行Feign的默认配置外,还可以直接通过配置文件进行Feign配置。可以通过feign.client.config.feignName.xxx配置名称为feignName的Feign Client的相应信息,比如下面代码中配置了名称为hello的Feign Client的相应信息。

feign.client.config.hello.decoder=com.elim.spring.cloud.client.HelloFeignConfiguration.HelloDecoder
feign.client.config.hello.loggerLevel=FULL
feign.client.config.hello.connectTimeout=1000
feign.client.config.hello.readTimeout=1000

loggerLevel是用来指定Feign Client进行请求时需要打印的日志信息类型,可选值有下面这几种,默认是NONE。对应的日志信息只有日志打印级别为DEBUG时才会生效。

    /**
    * No logging.
    */
   NONE,
   /**
    * Log only the request method and URL and the response status code and execution time.
    */
   BASIC,
   /**
    * Log the basic information along with request and response headers.
    */
   HEADERS,
   /**
    * Log the headers, body, and metadata for both requests and responses.
    */
   FULL

当同时定义了@Configuration对应的bean和通过配置文件定义的属性时,默认通过配置文件定义的属性将拥有更高的优先级,如果需要使通过Java代码配置的配置拥有更高的优先级可以配置feign.client.default-to-properties=false

基于Feign Client的配置信息由org.springframework.cloud.openfeign.FeignClientProperties负责接收,可以配置的信息请参考org.springframework.cloud.openfeign.FeignClientProperties.FeignClientConfiguration的源码或API文档。可以把feignName替换为default,此时对应的Feign Client的配置将作为默认的配置信息。

feign.client.config.default.decoder=com.elim.spring.cloud.client.HelloFeignConfiguration.HelloDecoder
feign.client.config.default.loggerLevel=FULL
feign.client.config.default.connectTimeout=1000
feign.client.config.default.readTimeout=1000

底层使用Apache Http Client时,如果需要对HttpClient进行自定义,除了定义自己的org.apache.http.impl.client.CloseableHttpClient类型的bean,还可以在application.properties文件中通过feign.httpclient.xxx属性进行配置。它们将由org.springframework.cloud.openfeign.support.FeignHttpClientProperties负责接收。比如下面的配置就自定义了HttpClient的连接配置。

feign.httpclient.maxConnections=200
feign.httpclient.maxConnectionsPerRoute=200
feign.httpclient.timeToLive=600

feign.httpclient.xxx只会对默认创建的CloseableHttpClient生效,如果自定义的CloseableHttpClient也希望响应通用的feign.httpclient.xxx参数,可以在创建自定义的CloseableHttpClient时注入FeignHttpClientProperties,从而读取对应的配置信息。

使用Spring Web的HttpMessageConverter

Spring Cloud Feign默认会使用基于Spring Web实现的org.springframework.cloud.openfeign.support.SpringEncoder进行编码,使用org.springframework.cloud.openfeign.support.SpringDecoder进行解码。它们底层使用的都是Spring Web的org.springframework.http.converter.HttpMessageConverter。SpringEncoder和SpringDecoder默认会被注入bean容器中所有的HttpMessageConverter,Spring Boot的自动配置会配置一些HttpMessageConverter。如果你想加入自己的HttpMessageConverter,只需要把它们定义为bean即可。下面代码中是一个自定义HttpMessageConverter的示例,它是基于MyObj进行转换的。

@Component
public class MyHttpMessageConverter extends AbstractHttpMessageConverter<MyObj> {
    private final StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
  
    public MyHttpMessageConverter() {
        super(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN);
    }
  
    @Override
    protected boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(MyObj.class);
    }
    @Override
    protected MyObj readInternal(Class<? extends MyObj> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        String text = this.stringHttpMessageConverter.read(String.class, inputMessage);
        return new MyObj(text);
    }
    @Override
    protected void writeInternal(MyObj t, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        this.stringHttpMessageConverter.write(t.getText(), MediaType.TEXT_PLAIN, outputMessage);
    }
  
    @Data
    public static class MyObj {
        private final String text;
    }
  
}

假设你的Feign Client中定义了下面这样一个接口定义。在进行远程调用时会先把方法参数通过上面的writeInternal(..)进行转换,获取了响应结果后,又会把响应结果通过上面的readInternal(..)转换为MyObj对象。

@PostMapping("hello/converter")
MyObj customHttpMessageConverter(@RequestBody MyObj obj);

Spring Web的HttpMessageConverter只对使用SpringEncoder或SpringDecoder生效,如果你使用了自定义的Encoder或Decoder它们就没用了。

RequestInterceptor

feign.RequestInterceptor是Feign为请求远程服务提供的拦截器,它允许用户在请求远程服务前对当前请求进行拦截并提供一些特定的信息。常见的场景是加入一些特定的Header。Spring Cloud Feign会自动添加bean容器中所有的feign.RequestInterceptor到请求拦截器列表中。下面的代码中我们定义了一个RequestInterceptor,并在每次请求中添加了头信息request-id

@Component
public class MyRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        template.header("request-id", UUID.randomUUID().toString());
    }
}

Feign提供了一个用来做基础认证的RequestInterceptor实现feign.auth.BasicAuthRequestInterceptor,如果远程接口需要使用Basic Auth时可以加入该RequestInterceptor定义,比如下面这样。

@Configuration
public class DefaultConfiguration {
    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("username", "password");
    }
  
}

拦截响应结果

Feign提供了RequestInterceptor对请求进行拦截,它允许我们对请求内容进行变更或者基于请求内容做一些事情。有时候可能你也想要对远程接口的响应结果进行一些处理,Feign没有直接提供这样的接口,Spring Cloud也没有提供这样的支持。幸运的是Feign的请求响应结果都将经过Decoder进行解码,所以如果想对响应结果进行拦截,可以实现自己的Decoder。假设我们底层还是希望使用默认的Decoder,底层默认使用的是被ResponseEntityDecoder包裹的SpringDecoder,那我们的自定义Decoder可以继承ResponseEntityDecoder,还是包裹SpringDecoder,那我们可以定义类似下面这样一个Decoder。下面的Decoder只是一个简单的示例,用来把每次请求的响应内容进行日志输出。

@Slf4j
public class LoggerDecoder extends ResponseEntityDecoder {
    public LoggerDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
        super(new SpringDecoder(messageConverters));
    }
    @Override
    public Object decode(Response response, Type type) throws IOException, FeignException {
        Object result = super.decode(response, type);
        log.info("请求[{}]的响应内容是:{}", response.request().url(), result);
        return result;
    }
}

然后就可以按照前面介绍的方式,在想要使用它的地方使用它了,可以配置为某个Feign Client专用,也可以是所有的Feign Client都默认使用的。

ErrorDecoder

当FeignClient调用远程服务返回的状态码不是200-300之间时,会抛出异常,对应的异常由feign.codec.ErrorDecoder进行处理后返回,默认的实现是feign.codec.ErrorDecoder.Default,其内部会决定是抛出可以重试的异常还是其它异常。如果你想进行一些特殊处理则可以定义自己的ErrorDecoder。

@Slf4j
public class MyErrorDecoder extends ErrorDecoder.Default {
    @Override
    public Exception decode(String methodKey, Response response) {
        Exception exception = super.decode(methodKey, response);
        log.error("请求{}调用方法{}异常,状态码:{}", response.request().url(), methodKey, response.status());
        return exception;
    }
}

然后可以把它定义为一个bean,或者通过配置文件指定使用它,如果需要通过配置文件指定,则可以进行类似如下这样。

feign.client.config.default.errorDecoder=com.elim.spring.cloud.client.config.MyErrorDecoder

Hystrix支持

当ClassPath下存在Hystrix相关的Class时,可以通过feign.hystrix.enabled=true启用对Hystrix的支持,此时Spring Cloud会把Feign Client的每次请求包装为一个HystrixCommand。所以此时也可以配置一些Hystrix相关的配置信息,比如超时时间、线程池大小等。比如下面定义了HystrixCommand的默认超时时间是3秒钟。

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=3000

如果想指定特定FeignClient的HystrixCommand配置,可以参考feign.hystrix.SetterFactory.Default.create(..)的源码其生成HystrixCommand的commandKey的方式。

也可以使用HystrixCommand的fallback,当断路器打开或者远程服务调用出错时将调用fallback对应的方法。FeignClient使用fallback时需要基于整个FeignClient接口指定fallback对应的Class。比如有如下这样一个FeignClient,我们通过fallback属性指定了fallback对应的Class。

@FeignClient(name="${feign.client.hello}", fallback=HelloServiceFallback.class)
public interface HelloService {
    @GetMapping("hello")
    String helloWorld();
  
    @GetMapping("hello/timeout/{timeout}")
    String timeout(@PathVariable("timeout") int timeout);
  
}

@FeignClient的fallback对应的Class需要实现@FeignClient标注的接口,对于上面的FeignClient,HelloServiceFallback类需要实现HelloService接口,此外fallback指定的Class需要是一个bean。HelloServiceFallback的示例代码如下。

@Component
public class HelloServiceFallback implements HelloService {
    @Override
    public String helloWorld() {
        return "fallback for helloWorld";
    }
    @Override
    public String timeout(int timeout) {
        return "fallback for timeout";
    }
}

如果希望在fallback方法中获取失败的原因,此时可以选择实现feign.hystrix.FallbackFactory接口,同时指定泛型类型为@FeignClient的接口类型,比如HelloService的FallbackFactory实现可以是如下这样。

@Component
public class HelloServiceFallbackFactory implements FallbackFactory<HelloService> {
    @Override
    public HelloService create(Throwable cause) {
        return new HelloService() {
            @Override
            public String helloWorld() {
                return "fallback for helloWorld,reason is:" + cause.getMessage();
            }
            @Override
            public String timeout(int timeout) {
                return "fallback for timeout, reason is :" + cause.getMessage();
            }
        };
    }
}

FallbackFactory的实现类需要定义为一个Spring bean。@FeignClient需要拿掉fallback属性,同时通过fallbackFactory属性指定对应的FallbackFactory实现类。

@FeignClient(name="${feign.client.hello}", fallbackFactory=HelloServiceFallbackFactory.class)
public interface HelloService {
    //...
}

当同时指定了fallback和fallbackFactory时,fallback拥有更高的优先级。 当使用了fallback时,由于fallback指定的Class实现了@FeignClient标注的接口,而且也定义为了Spring bean,那么Spring bean容器中同时会拥有多个@FeignClient标注的接口类型的bean。那通过@Autowired进行注入时就会报错,考虑到这种情况,Spring Cloud Feign默认把@FeignClient标注的接口生成的代理类bean标注为@Primary,即通过@Autowired注入的默认是@FeignClient对应的代理类,如果不希望该代理类bean是Primary,可以通过@FeignClient(primary=false)定义。

对请求或响应内容压缩

Feign可以通过如下方式配置是否需要对请求和响应的内容进行GZIP压缩,默认是不压缩的,如下则指定了请求和响应内容都需要压缩。

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

可以通过feign.compression.request.mime-types指定需要压缩的请求类型,通过feign.compression.request.min-request-size指定需要压缩的请求内容的最小值,以下是它们的默认值。

feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

请求内容的压缩由org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration进行自动配置。

自动重试

由于Feign Client底层使用的是Ribbon,所以Feign Client的自动重试与Ribbon的自动重试是一样的,Ribbon的自动重试之前笔者写的《客户端负载工具Ribbon》一文有描述,这里就不再赘述了。

参考文档