Feign Client 原理和使用

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

一、原理

Feign 是一个 Java 到 HTTP 的客户端绑定器,灵感来自于 RetrofitJAXRS-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

二、处理过程图

feign client 处理流程图

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

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

    Wechat

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,302评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,563评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,433评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,628评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,467评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,354评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,777评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,419评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,725评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,768评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,543评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,387评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,794评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,032评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,305评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,741评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,946评论 2 336