Feign Client 原理和使用

公眾號:好奇心森林

​關注他

創作宣告:內容包含虛構創作
6 人贊同了該文章

最近一個新專案在做後端HTTP庫技術選型的時候對比了Spring WebClient,Spring RestTemplate,Retrofit,Feign,Okhttp。綜合考慮最終選擇了上層封裝比較好的Feign,儘管我們的App沒有加入微服務,但是時間下來Feign用著還是很香的。

我們的sytyale針對Feign的底層原理和原始碼進行了解析,最後用一個小例子總結怎麼快速上手。

本文作者:sytyale,另外一個聰明好學的同事

一、原理

Feign 是一個 Java 到 HTTP 的客戶端繫結器,靈感來自於 Retrofit 和 JAXRS-2.0 以及 WebSocket。Feign 的第一個目標是降低將 Denominator 無變化的繫結到 HTTP APIs 的複雜性,而不考慮 ReSTfulness

Feign 使用 Jersey 和 CXF 等工具為 ReST 或 SOAP 服務編寫 java 客戶端。此外,Feign 允許您在 Apache HC 等http 庫之上編寫自己的程式碼。Feign 以最小的開銷將程式碼連線到 http APIs,並通過可定製的解碼器和錯誤處理(可以寫入任何基於文字的 http APIs)將程式碼連線到 http APIs。

Feign 通過將註解處理為模板化請求來工作。引數在輸出之前直接應用於這些模板。儘管 Feign 僅限於支援基於文字的 APIs,但它極大地簡化了系統方面,例如重放請求。此外,Feign 使得對轉換進行單元測試變得簡單。

Feign 10.x 及以上版本是在 Java 8上構建的,應該在 Java 9、10 和 11上工作。對於需要 JDK 6相容性的使用者,請使用 Feign 9.x

二、處理過程圖

三、Http Client 依賴

feign 在預設情況下使用 JDK 原生的 URLConnection 傳送HTTP請求。(沒有連線池,保持長連線) 。

可以通過修改 client 依賴換用底層的 client,不同的 http client 對請求的支援可能有差異。具體使用示例如下:

feign:
httpclient:
enable: false
okhttp:
enable: true

AND

<!-- Support PATCH Method-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency> <!-- Do not support PATCH Method -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>

四、Http Client 配置

  • okhttp 配置原始碼
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
public class OkHttpFeignConfiguration { private okhttp3.OkHttpClient okHttpClient; @Bean
@ConditionalOnMissingBean(ConnectionPool.class)
public ConnectionPool httpClientConnectionPool(
FeignHttpClientProperties httpClientProperties,
OkHttpClientConnectionPoolFactory connectionPoolFactory) {
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
} @Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
ConnectionPool connectionPool,
FeignHttpClientProperties httpClientProperties) {
Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
this.okHttpClient = httpClientFactory
.createBuilder(httpClientProperties.isDisableSslValidation())
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.followRedirects(followRedirects).connectionPool(connectionPool).build();
return this.okHttpClient;
} @PreDestroy
public void destroy() {
if (this.okHttpClient != null) {
this.okHttpClient.dispatcher().executorService().shutdown();
this.okHttpClient.connectionPool().evictAll();
}
}
}
  • HttpClient 配置原始碼
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(CloseableHttpClient.class)
public class HttpClientFeignConfiguration { private final Timer connectionManagerTimer = new Timer(
"FeignApacheHttpClientConfiguration.connectionManagerTimer", true); private CloseableHttpClient httpClient; @Autowired(required = false)
private RegistryBuilder registryBuilder; @Bean
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
public HttpClientConnectionManager connectionManager(
ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(httpClientProperties.isDisableSslValidation(),
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(),
httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
this.connectionManagerTimer.schedule(new TimerTask() {
@Override
public void run() {
connectionManager.closeExpiredConnections();
}
}, 30000, httpClientProperties.getConnectionTimerRepeat());
return connectionManager;
} @Bean
@ConditionalOnProperty(value = "feign.compression.response.enabled",
havingValue = "true")
public CloseableHttpClient customHttpClient(
HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {
HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement()
.useSystemProperties();
this.httpClient = createClient(builder, httpClientConnectionManager,
httpClientProperties);
return this.httpClient;
} @Bean
@ConditionalOnProperty(value = "feign.compression.response.enabled",
havingValue = "false", matchIfMissing = true)
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {
this.httpClient = createClient(httpClientFactory.createBuilder(),
httpClientConnectionManager, httpClientProperties);
return this.httpClient;
} private CloseableHttpClient createClient(HttpClientBuilder builder,
HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {
RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(httpClientProperties.getConnectionTimeout())
.setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
CloseableHttpClient httpClient = builder
.setDefaultRequestConfig(defaultRequestConfig)
.setConnectionManager(httpClientConnectionManager).build();
return httpClient;
} @PreDestroy
public void destroy() throws Exception {
this.connectionManagerTimer.cancel();
if (this.httpClient != null) {
this.httpClient.close();
}
}
}
  • HttpClient 配置屬性
@ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties { /**
* Default value for disabling SSL validation.
*/
public static final boolean DEFAULT_DISABLE_SSL_VALIDATION = false; /**
* Default value for max number od connections.
*/
public static final int DEFAULT_MAX_CONNECTIONS = 200; /**
* Default value for max number od connections per route.
*/
public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 50; /**
* Default value for time to live.
*/
public static final long DEFAULT_TIME_TO_LIVE = 900L; /**
* Default time to live unit.
*/
public static final TimeUnit DEFAULT_TIME_TO_LIVE_UNIT = TimeUnit.SECONDS; /**
* Default value for following redirects.
*/
public static final boolean DEFAULT_FOLLOW_REDIRECTS = true; /**
* Default value for connection timeout.
*/
public static final int DEFAULT_CONNECTION_TIMEOUT = 2000; /**
* Default value for connection timer repeat.
*/
public static final int DEFAULT_CONNECTION_TIMER_REPEAT = 3000; private boolean disableSslValidation = DEFAULT_DISABLE_SSL_VALIDATION; private int maxConnections = DEFAULT_MAX_CONNECTIONS; private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE; private long timeToLive = DEFAULT_TIME_TO_LIVE; private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT; private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS; private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT; //省略 setter 和 getter 方法
}

五、部分註解

  • FeignClient 註解原始碼
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient { // 忽略了過時的屬性 /**
* The name of the service with optional protocol prefix. Synonym for {@link #name()
* name}. A name must be specified for all clients, whether or not a url is provided.
* Can be specified as property key, eg: ${propertyKey}.
* @return the name of the service with optional protocol prefix
*/
@AliasFor("name")
String value() default ""; /**
* This will be used as the bean name instead of name if present, but will not be used
* as a service id.
* @return bean name instead of name if present
*/
String contextId() default ""; /**
* @return The service id with optional protocol prefix. Synonym for {@link #value()
* value}.
*/
@AliasFor("value")
String name() default ""; /**
* @return the <code>@Qualifier</code> value for the feign client.
*/
String qualifier() default ""; /**
* @return an absolute URL or resolvable hostname (the protocol is optional).
*/
String url() default ""; /**
* @return whether 404s should be decoded instead of throwing FeignExceptions
*/
boolean decode404() default false; /**
* A custom configuration class for the feign client. Can contain override
* <code>@Bean</code> definition for the pieces that make up the client, for instance
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
*
* @see FeignClientsConfiguration for the defaults
* @return list of configurations for feign client
*/
Class<?>[] configuration() default {}; /**
* Fallback class for the specified Feign client interface. The fallback class must
* implement the interface annotated by this annotation and be a valid spring bean.
* @return fallback class for the specified Feign client interface
*/
Class<?> fallback() default void.class; /**
* Define a fallback factory for the specified Feign client interface. The fallback
* factory must produce instances of fallback classes that implement the interface
* annotated by {@link FeignClient}. The fallback factory must be a valid spring bean.
*
* @see feign.hystrix.FallbackFactory for details.
* @return fallback factory for the specified Feign client interface
*/
Class<?> fallbackFactory() default void.class; /**
* @return path prefix to be used by all method-level mappings. Can be used with or
* without <code>@RibbonClient</code>.
*/
String path() default ""; /**
* @return whether to mark the feign proxy as a primary bean. Defaults to true.
*/
boolean primary() default true;
}

六、Feign Client 配置

  • FeignClient 配置原始碼
 /**
* Feign client configuration.
*/
public static class FeignClientConfiguration { private Logger.Level loggerLevel; private Integer connectTimeout; private Integer readTimeout; private Class<Retryer> retryer; private Class<ErrorDecoder> errorDecoder; private List<Class<RequestInterceptor>> requestInterceptors; private Boolean decode404; private Class<Decoder> decoder; private Class<Encoder> encoder; private Class<Contract> contract; private ExceptionPropagationPolicy exceptionPropagationPolicy; //省略setter 和 getter
}

七、Spring boot 服務下使用示例

  • pom.xml 中引入依賴,部分特性需要額外的依賴擴充套件(諸如表單提交等)
    <dependencies>
    <!-- spring-cloud-starter-openfeign 支援負載均衡、重試、斷路器等 -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.2.RELEASE</version>
    </dependency>
    <!-- Required to use PATCH. feign-okhttp not support PATCH Method -->
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>11.0</version>
    </dependency>
    </dependencies>
  • 開啟支援-使用 EnableFeignClients 註解
    @SpringBootApplication
    @EnableFeignClients
    public class TyaleApplication {

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

    }
  • 介面註解-標記請求地址、請求header、請求方式、引數(是否必填)等
    //如果是微服務內部呼叫則 value 可以直接指定對方服務在服務發現中的服務名,不需要 url
    @FeignClient(value = "tyale", url = "${base.uri}")
    public interface TyaleFeignClient {

    @PostMapping(value = "/token", consumes ="application/x-www-form-urlencoded")
    Map<String, Object> obtainToken(Map<String, ?> queryParam);

    @GetMapping(value = Constants.STATION_URI)
    StationPage stations(@RequestHeader("Accept-Language") String acceptLanguage,
    @RequestParam(name = "country") String country,
    @RequestParam(name = "order") String order,
    @RequestParam(name = "page", required = false) Integer page,
    @RequestParam(name = "pageSize") Integer pageSize);

    @PostMapping(value = Constants.PAYMENT_URI)
    PaymentDTO payment(@RequestHeader("Accept-Language") String acceptLanguage,
    @RequestBody PaymentRQ paymentRq);
    }
  • FormEncoder 支援
    @Configuration
    public class FeignFormConfiguration {

    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;

    @Bean
    @Primary
    public Encoder feignFormEncoder() {
    return new FormEncoder(new SpringEncoder(this.messageConverters));
    }
    }
  • 攔截器-自動新增header 或者 token 等
    @Configuration
    public class FeignInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
    requestTemplate.header(Constants.TOKEN_STR, "Bearer xxx");
    }
    }
  • ErrorCode-可以自定義錯誤響應碼的處理
    @Configuration
    public class TyaleErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
    TyaleErrorException errorException = null;
    try {
    if (response.body() != null) {
    Charset utf8 = StandardCharsets.UTF_8;
    var body = Util.toString(response.body().asReader(utf8));
    errorException = GsonUtils.fromJson(body, TyaleErrorException.class);
    } else {
    errorException = new TyaleErrorException();
    }
    } catch (IOException ignored) {

    }
    return errorException;
    }
    }
  • TyaleErrorException 類示例-處理返回失敗響應碼時的資料,不同的服務端可能需要不同的處理
    @EqualsAndHashCode(callSuper = true)
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class TyaleErrorException extends Exception {

    /**
    * example: "./api/{service-name}/{problem-id}"
    */
    private String type;

    /**
    * example: {title}
    */
    private String title;

    /**
    * example: https://api/docs/index.html#error-handling
    */
    private String documentation;

    /**
    * example: {code}
    */
    private String status;
    }
  • FeignClient 使用示例
    @RestController
    @RequestMapping(value = "/rest/tyale")
    public class TyaleController {

    @Autowired
    private TyaleFeignClient feignClient;

    @GetMapping(value="/stations")
    public BaseResponseDTO<StationPage> stations() {
    try {
    String acceptLanguage = "en";
    String country = "DE";
    String order = "NAME";
    Integer page = 0;
    Integer pageSize = 20;
    StationPage stationPage = feignClient.stations(acceptLanguage,
    country, order, page, pageSize);
    return ResponseBuilder.buildSuccessRS(stationPage);
    } catch (TyaleErrorException tyaleError) {
    System.out.println(tyaleError);
    //todo 處理異常返回時的響應
    }
    return ResponseBuilder.buildSuccessRS();
    }
    }