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. 段间跳转之TSS段

    TR寄存器,TSS描述符,TSS段 TR寄存器与普通的段寄存器一样都有可见部分和不可见部分.TR的可见部分为16位为其段选择子,不可见部分是32位的TSS基地址和16位的大小. TSS描述符存在GDT ...

  2. C++逆向分析----虚函数与多层继承

    虚函数 C++通过关键字virtual来将函数声明为一个虚函数.当一个类包含虚函数后编译器就会给类的实例对象增加一个虚表指针,所谓的虚表指针就是指向虚表的指针.虚表就是一张地址表,它包含了这个类中所有 ...

  3. BUAA软件工程_软件案例分析

    写在前面 项目 内容 所属课程 2020春季计算机学院软件工程(罗杰 任健) (北航) 作业要求 软件案例分析 课程目标 培养软件开发能力 本作业对实现目标的具体作用 对案例进行分析以学习软件开发的经 ...

  4. CRM系统实施的原则

    在我们使用CRM系统服务企业和客户之前,需要先系统的实施它.使用CRM系统却没有发挥它应有价值的案例很多,那么我们要怎样才能让CRM的作用发挥到最大,并确保它是成功的?那么今天小编跟您聊一聊,您的企业 ...

  5. ES6学习-2 let

    ES6 新增了let命令,用来声明变量.它的用法类似于var,但是let所声明的变量,只在let命令所在的代码块内有效. 1 { 2 let a = 10; 3 var b = 1; 4 } 5 co ...

  6. MSSQL·查询T-SQL语句执行时间的三种方法

    阅文时长 | 0.23分钟 字数统计 | 420.8字符 主要内容 | 1.引言&背景 2.自定义时间变量求差法 3.MSSQL内置方法 4.MSSQL选项开启时间统计 5.声明与参考资料 『 ...

  7. Tomcat的使用和配置

    Tomcat的使用 安装 在tomcat官网找到你需要用的 Tomcat 版本对应的 zip 压缩包,解压到需要安装的目录即可 目录介绍 bin : 专门用来存放Tomcat服务器的可执行文件 con ...

  8. [DB] 大数据概述

    什么是大数据 电商推荐系统 大量订单如何存储(十年) 大量的订单如何计算(不关心算法) 天气预报 大量的天气数据如何存储 大量天气数据如何计算 核心问题 数据的存储:分布式文件系统(HDFS) 数据的 ...

  9. tar解压某个目录 tar解压某个指定的文件或者文件夹

    tar解压某个目录 tar解压某个指定的文件或者文件夹 发布时间:2017-05-30 来源:服务器之家   1. 先查看压缩文档中有那些文件,如果都不清楚文件内容,然后就直接解压,这个是不可能的 使 ...

  10. Linux 操作系统(四)用户组管理&进程管理&任务调度

    以下实例均在Centos7下验证 Centos7 查看命令帮助 man xxx 用户组管理 useradd useradd user1 password user1 cat /etc/passwd # ...