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. uboot1: 启动流程和移植框架

    目录 0 环境 1 移植框架 3 执行流程 3.0 链接地址 3.1 start.S, 入口 3.2 __main 3.3 board_init_f()和init_sequence_f[] 3.4 r ...

  2. checked 和 prop() (散列性比较少的)

    在<input  class="sex1" type="radio" checked>男 checked表示该框会被默认选上 prop()操作的是D ...

  3. VSCode·备份&还原配置及拓展项

    阅文时长 | 0.54分钟 字数统计 | 924字符 主要内容 | 1.引言&背景 2.备份VSCode配置 3.还原VSCode配置 4.Syncing常用命令 5.声明与参考资料 『VSC ...

  4. [bug] ORACLE not available

    参考 https://www.cnblogs.com/sank/p/10046277.html

  5. Zabbix 架构

    Zabbix 架构 1.Zabbix Server Zabblx server是agent程序报告系统可用性.系统完整性和统计数据的核心组件,是所有配置信息.统计信息和操作数据的核心存储器. 2.Za ...

  6. 11.6 mpstat:CPU信息统计

        mpstat 是Multiprocessor Statistics的缩写,是一种实时系统监控工具.mpstat命令会输出CPU的一些统计信息,这些信息存放在/proc/stat文件中.在多CP ...

  7. cgic: CGI的C函数库-(转自COS)

    下载回源码包以后,就3个文件:cgic.c      函数库capture.c   一个很简单的CGI例子,仅仅输出两行提示文字cgictest.c  一个演示读取form表单数据的CGI例子 首先在 ...

  8. linux 系统监控命令之 top-(转自 Howie的专栏)

    top命令经常用来监控linux的系统状况,比如cpu.内存的使用,程序员基本都知道这个命令,但比较奇怪的是能用好它的人却很少,例如top监控视图中内存数值的含义就有不少的曲解. 本文通过一个运行中的 ...

  9. 删除本地解压版Mysql

    1.关闭服务 以管理员身份运行cmd,使用命令net stop mysql停止服务. 2.卸载服务 使用命令mysqld -remove mysql卸载服务. 这时候在服务里已经找不到mysql服务了 ...

  10. 设计模式Copy-on-write

    1.Copy-on-Write 又称COW,写时复制 String的replace()方法,没有修改内部的value数组,而是新创建了一个不可变对象 这种方法在解决不可变对象时,经常使用 这其实就是一 ...