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

Feign Client 原理和使用的更多相关文章

  1. No fallback instance of type class found for feign client user-service(转)

    1.错误日志 在 feign 开启熔断,配置 fallback 类,实现当前接口的实现类时,报错信息如下: Error starting ApplicationContext. To display ...

  2. Feign 系列(03)Feign 工作原理

    目录 Feign 系列(03)Feign 工作原理 1. Feign 是如何设计的 2. Feign 动态代理 2.1 ReflectiveFeign 构建 2.2 生成代理对象 2.3 Method ...

  3. Spring Cloud 整合 Feign 的原理

    前言 在 上篇 介绍了 Feign 的核心实现原理,在文末也提到了会再介绍其和 Spring Cloud 的整合原理,Spring 具有很强的扩展性,会把一些常用的解决方案通过 starter 的方式 ...

  4. spring cloud feign覆写默认配置级feign client的日志打印

    一.覆写fegin的默认配置 1.新增配置类FeignConfiguration.java package com.config; import org.springframework.context ...

  5. feign client 的简单使用(1)

    依赖: <properties> <java.version>1.8</java.version> <feign-core.version>10.2.0 ...

  6. No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?

    ... 60 common frames omittedCaused by: java.lang.IllegalStateException: No Feign Client for loadBala ...

  7. Spring Could Feign 设计原理

    什么是Feign? Feign 的英文表意为"假装,伪装,变形", 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HT ...

  8. Spring Cloud Feign设计原理

    什么是Feign? Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直 ...

  9. feign架构 原理解析

    什么是feign? 来自官网的解释:Feign makes writing java http clients easier 在使用feign之前,我们怎么发送请求? 拿okhttp举例: publi ...

随机推荐

  1. Pytorch系列:(五)CNN

    卷积 Conv2d 2D卷积函数和参数如下 nn.Conv2d( in_channels, out_channels, kernel_size, stride=1, padding=0, dilati ...

  2. MySQL如何快速插入数据

    前言: 日常学习和工作中,经常会遇到导数据的需求.比如数据迁移.数据恢复.新建从库等,这些操作可能都会涉及大量数据的导入.有时候导入进度慢,电脑风扇狂转真的很让人崩溃,其实有些小技巧是可以让导入更快速 ...

  3. CLS的探索:Python如何让日志免费云化

    前言 日志服务(Cloud Log Service,CLS)是腾讯云提供的一站式日志服务平台,提供了从日志采集.日志存储到日志检索,图表分析.监控告警.日志投递等多项服务,协助用户通过日志来解决业务运 ...

  4. class的大小

    3个问题: sizeof一个空类是多大?为什么?编译器为什么这么做? 在这个类中添加一个virtual函数后再sizeof,这时是多大?为什么? 将这个类再virtual继承一个其它的空类,这是多大? ...

  5. [BUAA2021软工助教]结对项目-第二阶段小结

    一.作业链接 结对项目-第二阶段 二.优秀作业推荐 本次博客作业虽然是简单总结,但是以下作业中都不乏有思考.有亮点的精彩内容,推荐给同学们阅读学习. 磨练,结对编程!(中) zzx 和 zzy 同学实 ...

  6. SQLFlow数据流分析工具的job功能介绍

    SQLFlow是一款专业的数据血缘关系分析工具,在大型数据仓库中,完整的数据血缘关系可以用来进行数据溯源.表和字段变更的影响分析.数据合规性的证明.数据质量的检查等. 一.SQLFlow 是怎样工作的 ...

  7. 【转载】在Linux系统下用dd命令制作ISO镜像U盘启动盘

    #### 将U盘插入USB接口 #umount /dev/sdb* #dd if=/iso存放路径/XXX.iso of=/dev/sdb bs=1M ##### [转载]在Linux系统下用dd命令 ...

  8. iozone的使用与介绍-20191105

    https://www.jianshu.com/p/faf82e400aa6 iozone的使用与介绍 0.0722017.05.10 07:40:41字数 550阅读 1817 iozone的使用与 ...

  9. Centos7如何安装开源办公软件Libreoffice

    在Centos7安装了WPS,但是用了没两月就出问题,无法正常使用.(准确来说,安装的WPS一直都有各种问题存在,但是没有影响到主要功能也就将就着用,后来是直接输入不了文字) 既然是开源系统,自然而然 ...

  10. Wrong——Python

    1.向MySQL中插入时间(年月日时分秒),在时分秒处报错.在年月日与时分秒中间有个空格.解决办法:在sql语句中时间用一个" "包起来就可以了.. 2.在Ubuntu的cront ...