Feign Client 原理和使用

发布于: 2020 年 06 月 21 日
Feign Client 原理和使用

最近一个新项目在做后端HTTP库技术选型的时候对比了Spring WebClient,Spring RestTemplate,Retrofit,Feign,Okhttp。综合考虑最终选择了上层封装比较好的Feign,尽管我们的App没有加入微服务,但是时间下来Feign用着还是很香的。



我们的sytyale针对Feign的底层原理和源码进行了解析,最后用一个小例子总结怎么快速上手。



本文作者:sytyale,另外一个聪明好学的同事



一、原理



Feign 是一个 Java 到 HTTP 的客户端绑定器,灵感来自于 Retrofit 和 [JAXRS-2.0](https://jax-rs-spec.java.net/nonav/2.0/apidocs/index.html) 以及 [WebSocket](http://www.oracle.com/technetwork/articles/java/jsr356-1937161.html)。Feign 的第一个目标是降低将 [Denominator](https://github.com/Netflix/Denominator) 无变化的绑定到 HTTP APIs 的复杂性,而不考虑 [ReSTfulness](http://www.slideshare.net/adrianfcole/99problems)



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 中引入依赖,部分特性需要额外的依赖扩展(诸如表单提交等)



```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 注解



```java

@SpringBootApplication

@EnableFeignClients

public class TyaleApplication {

public static void main(String[] args) {

SpringApplication.run(TyaleApplication.class, args);

}

}

```



  • 接口注解-标记请求地址、请求header、请求方式、参数(是否必填)等



```java

//如果是微服务内部调用则 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 支持



```java

@Configuration

public class FeignFormConfiguration {

@Autowired

private ObjectFactory<HttpMessageConverters> messageConverters;

@Bean

@Primary

public Encoder feignFormEncoder() {

return new FormEncoder(new SpringEncoder(this.messageConverters));

}

}

```



  • 拦截器-自动添加header 或者 token



```java

@Configuration

public class FeignInterceptor implements RequestInterceptor {

@Override

public void apply(RequestTemplate requestTemplate) {

requestTemplate.header(Constants.TOKEN_STR, "Bearer xxx");

}

}

```



  • ErrorCode-可以自定义错误响应码的处理



```java

@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 类示例-处理返回失败响应码时的数据,不同的服务端可能需要不同的处理



```java

@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 使用示例



```java

@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();

}

}

```



查看更多文章关注公众号:好奇心森林



发布于: 2020 年 06 月 21 日 阅读数: 80
用户头像

还未添加个人签名 2017.10.17 加入

11年入行互联网,关注技术和金融。

评论

发布
暂无评论
Feign Client 原理和使用