Spring Cloud(10)——聲明式的Rest客戶端Feign

聲明式的Rest客戶端

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》一文有描述,這裏就不再贅述了。

參考文檔

(注:本文是基於Spring cloud Finchley.SR1所寫)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章