Feign入门介绍

Feign入门介绍

基本概述

除Feign之外,在Java中经常使用的HTTP客户端组件主要有3个,如下:
(1)HttpURLConnection,JDK自带
(2)Apache HttpClient,独立的HTTP客户端实现,使用广泛,目前已发展到5.x版本,详见:https://hc.apache.org/index.html
(3)OkHttp,一个新出现的HTTP客户端,详见:https://square.github.io/okhttp/

为什么已经存在了如Apache HttpClient和OkHttp这样优秀HTTP客户端组件了,还会出现Feign呢?难道不是重复发明轮子吗?

实际上,Feign并没有去做跟Apache HttpClient或OkHttp一样重复的事情,而是一个Http客户端框架,用于集成诸如URLConnection,Apache HttpClient,OkHttp这样的Http客户端实现。
这种关系就像Slf4j跟Log4j,Logback一样:Slf4j提供了一套日志API,而具体的日志实现可以是Log4j或者Logback,参考细说java平台日志组件
Feign架构图

那么,Feign作为一个框架组件,给开发者带来了哪些便利呢?使用Feign和不使用Feign有什么区别呢?如下一一道来。

如何使用Feign

Feign框架是一个基于“Apache-2.0 License”协议的开源项目,地址:https://github.com/OpenFeign/feign
使用Feign有2种方式:独立使用Feign,在Spring框架中集成Feign。

独立使用Feign

基础用法

独立使用Feign框架是最简单,也是最基础的使用方式,掌握了如何独立使用Feign框架,才能对在Spring框架中集成使用Feign有更加清晰的理解。

第一步:下载Feign
Feign框架最简单的用法,只需要下载核心Jar包即可,Maven依赖配置如下:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-core</artifactId>
    <version>{version}</version>
</dependency>

第二步:定义业务接口
在业务接口中通过注解来定义详细请求信息,Feign默认提供的注解有6个:

注解 使用位置 说明
@RequestLine 方法 指定HTTP方法和请求路径
@Param 参数 指定单个表单参数,只能用在POST请求中
@Headers 方法,接口 指定单个HTTP请求头,在接口上使用时为所有方法指定相同的消息头,在方法上使用时只为该方法自身指定消息头
@QueryMap 参数 指定批量动态查询URL参数,被标注的对象可以是一个Map集合,也可以是一个POJO对象
@HeaderMap 参数 指定批量HTTP请求头参数
@Body 方法 为PUT或POST方法指定一个模板消息体

如下示例:

public interface MyAPI {
    @RequestLine("GET /test/ping")
    String ping();
}

第三步:调用接口方法

# 通过Feign调用接口“http://localhost:8080/test/ping”
MyAPI myAPI = Feign.builder().target(MyAPI.class, "http://localhost:8080");
String result = myAPI.ping();
System.out.println(result);

如上,关于Feign的基础用法就2点:
1.定义接口及方法,并在接口(方法)上通过Feign提供的注解配置请求详情
2.构建Feign对象,请求目标接口

模块介绍及作用

Feign框架自身包含多个模块,这些模块的作用是为了实现Feign与其他组件的集成。
主要模块如下:

  • feign-gson:集成Gson组件,用于实现Json格式的请求编码和响应解码
  • feign-jackson:集成jackson-databind组件,用于实现Json格式的请求编码和响应解码
  • feign-okhttp:集成OkHttp组件,使用OkHttp作为底层通信实现
  • feign-httpclient:集成HttpClient组件,使用HttpClient作为底层通信实现
  • feign-ribbon:集成Netflix/ribbon组件,实现请求负责均衡
  • feign-hystrix:集成Netflix/Hystrix组件,实现异常熔断
  • feign-slf4j:集成SLF4J日志框架,可以灵活地实现日志信息的输出
  • feign-form:feign-form是Feign提供的一个独立的组件,用于实现表单格式的请求参数传递

自定义Feign

了解了Feign的基础用法还不够,在实际场景当中,有许多复杂的需求,比如:
1.目标接口如果返回的是json对象,默认情况下Feign是不支持的,请求时将会报错。
2.如果希望请求接口的时候发送的是json对象,默认情况下Feign也不支持。
3.请求的目标地址是动态变化的,该如何实现呢?

诸如此类。。。
如果Feign连这些需求都满足不了,那自然就没有什么可以学习的价值了。反之,Feign是一个支持高度定制化的框架,可以满足各种各样的应用场景。
如下是Feign.builder类的属性定义,自定义Feign主要是围绕这些属性来实现的。

public abstract class Feign {
    public static class Builder {
        private final List<RequestInterceptor> requestInterceptors = new ArrayList<RequestInterceptor>();
        private Logger.Level logLevel = Logger.Level.NONE;
        private Contract contract = new Contract.Default();
        private Client client = new Client.Default(null, null);
        private Retryer retryer = new Retryer.Default();
        private Logger logger = new NoOpLogger();
        private Encoder encoder = new Encoder.Default();
        private Decoder decoder = new Decoder.Default();
        private QueryMapEncoder queryMapEncoder = new FieldQueryMapEncoder();
        private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
        private Options options = new Options();
        private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default();
        private boolean decode404;
        private boolean closeAfterDecode = true;
        private ExceptionPropagationPolicy propagationPolicy = NONE;
        private boolean forceDecoding = false;
        private List<Capability> capabilities = new ArrayList<>();
    }
}

在详细讲解如何定制Feign之前,先要弄明白其运行机制。
先看一下Feign的初始化序列图,如下所示:
Feign初始化时序图

从序列图中可以很清楚地看到,Feign初始化后返回一个Proxy动态代理对象,很显然在请求时正是通过这个代理对象执行的。
另外,为了清楚Feign可以定制的要素有哪些,再看一下其运行时序图,如下所示:
Feign运行时序图

从Feign的运行时序图中可以看到,在一次请求过程中最重要的三个组件分别是;Encoder(请求参数编码器),Client(Http客户端实现),Decoder(响应结果解码器)。
所以,对Feign定制时最核心的就是如下三个属性:encoder,decoder,client。此外,retryer(重试策略)也是Feign框架的非常重要的属性,它大大简化了异常重试的实现逻辑。

设置超时时间

关于超时时间,是指底层Socket通信的选项,Feign同时支持设置连接超时时间和读取超时时间,这2个参数最终都设置在具体的HTTP实现组件上生效的(比如:HttpClient,OkHttp)。
另外,作为HTTP协议特有的3xx请求重定向状态码处理,Feign也支持通过参数化设置,这将大大简化在底层通信层面上处理这些业务逻辑。
超时时间的设置通过options属性设置,如下示例:

# 设置连接超时和读取超时时间为10秒,同时设置对返回3xx状态码做重定向处理
MyAPI myAPI = Feign.builder()
                .options(new Request.Options(10, TimeUnit.SECONDS, 10, TimeUnit.SECONDS, true))
                .target(MyAPI.class, "http://localhost:8080");
请求参数编码器

编码器决定了使用何种方式对请求消息体进行编码后发送给目标地址,通常来讲,HTTP请求的消息体有如下4种编码格式:
1.表单格式:application/x-www-form-urlencoded,这种方式将请求参数以键值对的形式发送给目标地址,多个键值对之间使用&分隔。

POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
key1=value1&key2=value2

2.JSON格式:application/json,这种方式支持传递JSON格式的数据,是目前使用得非常广泛的格式。

POST http://www.example.com HTTP/1.1 
Content-Type: application/json;charset=utf-8
{"title":"test","sub":[1,2,3]}

3.XML格式:text/xml,这种格式已经不再常用,但还常用来作为配置文件。

POST http://www.example.com HTTP/1.1 
Content-Type: text/xml
<?xml version="1.0"?>
<methodCall>
    <methodName>examples.getStateName</methodName>
    <params>
        <param>
            <value><i4>41</i4></value>
        </param>
    </params>
</methodCall>

4.文件上传格式:multipart/form-data,这种格式通常在文件上传的场景中使用。

POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"
title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png
... content of chrome.png(省略) ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

有意思的是,在Feign中使用何种方式对请求消息进行编码是由业务接口方法的定义决定的,核心逻辑在ReflectiveFeign.ParseHandlersByName.apply()中,如下所示:

public Map<String, MethodHandler> apply(Target target) {
    // 解析所有业务接口方法的元数据信息
    List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
    Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
    for (MethodMetadata md : metadata) {
        BuildTemplateByResolvingArgs buildTemplate;
        // 根据接口方法元数据创建对应的ReflectiveFeign.BuildTemplateByResolvingArgs对象
        // 这里很关键,在这里决定了执行HTTP请求时对消息体进行编码的方式
        if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
            // 在业务接口方法中有参数,并且使用了参数注解:@Param
            // 调用编码器对象对请求消息体进行编码
            buildTemplate =
                new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
        } else if (md.bodyIndex() != null) {
            // 在业务接口方法中有参数,但是未使用任何参数注解:@Param,@QueryMap,@HeaderMap
            // 调用编码器对象对请求消息体进行编码
            buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
        } else {
            // 在业务接口方法中没有参数,或者有参数但是未使用参数注解@Param
            // 不会调用编码器对象对请求消息体进行编码,但是会将@QueryMap注解指定的集合对象中的数据拼接为URL参数
            buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
        }
        // 部分后续代码省略
    }
    return result;
}

如下为ReflectiveFeign.BuildTemplateByResolvingArgs类图:
BuildTemplateByResolvingArgs类图

继续追踪源码可以知道:ReflectiveFeign.BuildFormEncodedTemplateFromArgs和ReflectiveFeign.BuildEncodedTemplateFromArgs都是通过编码器对象对请求消息体进行编码的。
ReflectiveFeign.BuildFormEncodedTemplateFromArgs源码:

@Override
protected RequestTemplate resolve(Object[] argv,
                                    RequestTemplate mutable,
                                    Map<String, Object> variables) {
    Map<String, Object> formVariables = new LinkedHashMap<String, Object>();
    for (Entry<String, Object> entry : variables.entrySet()) {
        if (metadata.formParams().contains(entry.getKey())) {
            formVariables.put(entry.getKey(), entry.getValue());
        }
    }
    try {
        // 调用编码器对象对请求消息进行编码
        encoder.encode(formVariables, Encoder.MAP_STRING_WILDCARD, mutable);
    } catch (EncodeException e) {
        throw e;
    } catch (RuntimeException e) {
        throw new EncodeException(e.getMessage(), e);
    }
    return super.resolve(argv, mutable, variables);
}  

ReflectiveFeign.BuildEncodedTemplateFromArgs源码:

@Override
protected RequestTemplate resolve(Object[] argv,
                                    RequestTemplate mutable,
                                    Map<String, Object> variables) {
    Object body = argv[metadata.bodyIndex()];
    checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
    try {
        // 调用编码器对象对请求参数进行编码
        encoder.encode(body, metadata.bodyType(), mutable);
    } catch (EncodeException e) {
        throw e;
    } catch (RuntimeException e) {
        throw new EncodeException(e.getMessage(), e);
    }
    return super.resolve(argv, mutable, variables);
}

值得注意的是,ReflectiveFeign.BuildTemplateByResolvingArgs却不会调用编码器对象进行编码。但是当在接口方法中使用了参数注解@QueryMap时,会将@QueryMap指定的map集合中的数据拼接为URL参数。
另外,当不明确指定编码器时,Feign默认的编码器只能处理字符串或者字节数组类型的消息体。

// Feign的默认编码器只能处理字符串或字节数组类型请求消息体
class Default implements Encoder {
    @Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
        if (bodyType == String.class) {
            // 请求消息体类型为字符串
            template.body(object.toString());
        } else if (bodyType == byte[].class) {
            // 请求消息体类型为字节数组
            template.body((byte[]) object, null);
        } else if (object != null) {
            throw new EncodeException(
                format("%s is not a type supported by this encoder.", object.getClass()));
        }
    }
}

综上所述,我们已经知道了Feign对请求消息体进行编码的处理逻辑。
那么如何明确设置编码器呢?又可以指定哪些编码呢?
实际上,Feign作为一个高级的HTTP客户端框架,既支持表单格式的消息体,也支持JSON格式和XML格式的消息体。
值得注意的是,Feign本身并没有为了支持JSON格式和XML格式的消息体而开发新的JSON或XML解析器,而是集成现有主流的JSON和XML编码器:
1.JSON格式编解码:Gson,Jackson 1,Jackson 2,Jackson Jr(移动端的Jackson),JSON-java
2.xml格式编解码:JAXB,Jackson JAXB,SAX(XML解码器)

如果要使用的特定的编解码器,需要在pom中引入对应的模块。
以JSON格式编码器为例,可以引入feign-gson模块:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-gson</artifactId>
    <version>${feign-gson-version}</version>
</dependency>

设置请求参数编码器:

MyAPI myAPI = Feign.builder()
                   .encoder(new GsonEncoder()) // 请求将作为JSON格式传递
                   .target(MyAPI.class, "http://localhost:8080");

如果需要按表单格式传递参数,需要引入feign-form模块:

<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form</artifactId>
    <version>3.8.0</version>
</dependency>

设置表单参数编码器:

public interface MyAPI {
    // 使用表单格式传递参数
    @RequestLine("POST /test/simple/post/form")
    @Headers("Content-Type: application/x-www-form-urlencoded")
    Object postForm(Subject subject);
}

// 将请求参数编码为表单格式
MyAPI myAPI = Feign.builder()
                   .encoder(new FormEncoder()) // 请求参数作为表单格式传递
                   .target(MyAPI.class, "http://localhost:8080");

特别注意
在使用FormEncoder作为请求编码器时,一定要明确设置请求头Content-Type为“application/x-www-form-urlencoded”,否则在执行时不会编码为表单格式。
与此同时可能会报错:

class xxx.xxx.xxx is not a type supported by this encoder.

原因在于FormEncoder在编码时会判断是否明确设置了请求内容类型,如果没有明确设置Content-Type,将会使用默认编码器对请求参数进行编码(只支持字符串和字节数组)。

public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException {
    // 如果不明确设置Content-Type值,contentTypeValue变量为null
    String contentTypeValue = getContentTypeValue(template.headers());
    val contentType = ContentType.of(contentTypeValue);
    // 在这里会明确判断请求内容类型,如果未明确设置Content-Type,这里contentType变量值为UNDEFINED,将会使用默认编码器
    if (!processors.containsKey(contentType)) {
      delegate.encode(object, bodyType, template);
      return;
    }

    // 省略部分代码
    // ...
}

关于请求参数编码器的总结:
(1)默认情况下,请求消息体只支持字符串或字节数组
(2)如果需要支持特定的请求消息体格式,如要引入对应模块,如:feign-gson,feign-jackson等等。

Feign官方支持的编解码器

响应结果解码器

与请求消息编码器对应的是响应结果解码器,即:Feign支持解析多种格式的HTTP响应消息体。
Feign的默认解码器是StringDecoder的子类,所以只能解码响应结果为字符串类型数据。如果响应结果为其他对象格式(如:JSON对象),在执行请求时将报错:“class xxx is not a type supported by this decoder.”。

public class StringDecoder implements Decoder {

  @Override
  public Object decode(Response response, Type type) throws IOException {
    Response.Body body = response.body();
    if (body == null) {
      return null;
    }
    if (String.class.equals(type)) {
      return Util.toString(body.asReader(Util.UTF_8));
    }
    throw new DecodeException(response.status(),
        format("%s is not a type supported by this decoder.", type), response.request());
  }
}

跟请求编码器类似,响应解码器也支持解码多种格式的响应数据,如:JSON,XML,字符串(默认)。

如下示例为明确配置Feign解析JSON格式的响应数据:
引入模块信息:

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-gson</artifactId>
    <version>${feign-gson-version}</version>
</dependency>
// 解码JSON格式响应数据
MyAPI myAPI = Feign.builder()
                   .encoder(new FormEncoder())
                   .decoder(new GsonDecoder())
                   .target(MyAPI.class, "http://localhost:8080");
集成其他HTTP组件

前面说过,Feign框架本身没有再去做底层通信的实现,仅仅是作为一个框架可以集成现有的HTTP组件。
默认情况下,Feign使用的JDK自带的HttpURLConnection作为底层通信组件。

class Default implements Client {
    HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
        final URL url = new URL(request.url());
        final HttpURLConnection connection = this.getConnection(url);
        // 省略部分源码
    }
}

如果想使用其他HTTP实现作为底层通信组件,Feign提供相应的模块进行支持。

<!-- 在Feign中使用Apache HttpClient -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    <version>11.1</version>
</dependency>

<!-- 在Feign中使用okhttp -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>11.1</version>
</dependency>

<!-- 在Feign中使用ribbon实现负载均衡 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-ribbon</artifactId>
    <version>11.1</version>
</dependency>
MyAPI myAPI = Feign.builder()
            .client(new ApacheHttpClient())  // 使用Apache HttpClient实现作为底层通信组件
            .client(new OkHttpClient())      // 使用OKHttp作为底层通信组件
            .client(RibbonClient.create())   // 使用Ribbon实现负载均衡,(注:Ribbon默认情况下底层使用HttpURLConnection作为通信组件)
            .client(new Http2Client())       // 自JDK11起,还可以直接使用Http2Client
            .target(MyAPI.class, "http://localhost:8080");

甚至还可以集成Netflix/Hystrix实现异常熔断。

<!-- 在Feign中使用Hystrix实现请求熔断 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-hystrix</artifactId>
    <version>11.8</version>
</dependency>
// 在Feign中使用Hystrix实现请求熔断
MyAPI api = HystrixFeign.builder()
                .client(new OkHttpClient())
                .target(MyAPI.class, "http://localhost:8080");
设置请求消息头

如果需要在发送请求时设置消息头,可以使用注解@Headers实现。

public interface MyAPI {
    // 使用注解@Headers明确指定请求参数按表单格式传递
    @RequestLine("POST /test/simple/post/form")
    @Headers("Content-Type: application/x-www-form-urlencoded")
    Object formPost(Subject subject);
}

另外,还可以通过自定义Target为所有请求都添加请求头。

static class UrlAndTokenProvider {
    public TokenIdAndPublicURL get() {
        TokenIdAndPublicURL tokenIdAndPublicURL = new TokenIdAndPublicURL();
        tokenIdAndPublicURL.publicURL = "http://localhost:8080";
        tokenIdAndPublicURL.tokenId   = "xxxxxxxxxxxxxxxxxxxxx";
        return tokenIdAndPublicURL;
    }
}

static class TokenIdAndPublicURL {
    public String publicURL;
    public String tokenId;
}

// 自定义Target,实现动态添加请求消息头
static class DynamicAuthTokenTarget<T> implements Target<T> {
    private Class clazz;
    private UrlAndTokenProvider provider;
    private ThreadLocal<String> requestIdProvider;
    public DynamicAuthTokenTarget(Class<T> clazz,
                                    UrlAndTokenProvider provider,
                                    ThreadLocal<String> requestIdProvider) {
        this.clazz = clazz;
        this.provider = provider;
        this.requestIdProvider = requestIdProvider;
    }

    public Class<T> type() {
        return this.clazz;
    }

    public String name() {
        return "DynamicAuthTokenTarget";
    }

    public String url() {
        return null;
    }

    // 该方法的调用是线程安全的,可以为每一个请求动态设置参数
    @Override
    public Request apply(RequestTemplate input) {
        TokenIdAndPublicURL urlAndToken = provider.get();
        if (input.url().indexOf("http") != 0) {
            input.insert(0, urlAndToken.publicURL);
        }
        // 动态添加请求消息头
        input.header("X-Auth-Token", urlAndToken.tokenId);
        input.header("X-Request-ID", requestIdProvider.get());
        return input.request();
    }
}

// 使用自定义Target实现动态添加请求消息头
ThreadLocal<String> requestIdProvider = new ThreadLocal<String>();
requestIdProvider.set("X-Request-ID-Value");
MyAPI api = Feign.builder()
        .target(new DynamicAuthTokenTarget<MyAPI>(MyAPI.class, new UrlAndTokenProvider(), requestIdProvider));

实际上,设置请求消息头除了可以使用注解@Headers,自定义Target外,还可以通过自定义请求拦截器RequestInterceptor实现。

请求拦截器

如果需要对所有请求都做需要做相同的设置,比如:添加请求头,设置请求参数签名值等,就非常适合通过请求拦截器来实现。
在Feign中设置请求拦截器是通过设置requestInterceptor属性实现的:

// 自定义请求拦截器
static class MyRequestInterceptor implements RequestInterceptor {
    public void apply(RequestTemplate template) {
        // 对所有请求设置消息头
        template.header("X-Forwarded-For", "origin.host.com");
        String md5 = "对请求参数计算签名值";
        // 对所有请求设置请求参数
        template.query("sign", md5);
    }
}

// 设置请求拦截器
MyAPI myAPI = Feign.builder()
                .requestInterceptor(new MyRequestInterceptor())
                .target(MyAPI.class, "http://localhost:8080");
动态查询参数

如果需要批量传递查询URL参数,可以使用Feign自带的注解@QueryMap实现,该注解标注的对象可以是一个Map集合,也可以是POJO对象。
示例如下:

public interface MyAPI {
    // 使用注解@QueryMap标注Map对象发送动态查询URL参数
    @RequestLine("GET /test/parammap/get")
    String paramMapGet(@QueryMap Map<String, Object> paramMap);

    // 使用注解@QueryMap标注POJO对象发送动态查询URL参数
    @RequestLine("GET /test/parammap/get")
    String paramPojoGet(@QueryMap Subject subject);
}

// 调用业务接口
MyAPI myAPI = Feign.builder().target(MyAPI.class, "http://localhost:8080");
// 动态查询参数
// 标注Map对象
Map<String, Object> paramMap = new HashMap<String, Object>();
paramMap.put("id", 10);
myAPI.paramMapGet(paramMap);

// 标注POJO对象
Subject subject = Subject.builder().id(10).build();
myAPI.paramPojoGet(subject);

默认情况下,Feign使用FieldQueryMapEncoder对查询参数进行编码,还可以自定义查询参数编码器。

MyAPI myAPI = Feign.builder()
        .queryMapEncoder(new MyQueryMapEncoder()) // 使用自定义查询参数编码器
        .target(MyAPI.class, "http://localhost:8080");
错误处理

错误处理是指Feign框架允许返回一个自定义的异常,具体来说就是允许配置一个ErrorDecoder对象进行处理。

// 配置错误解码器
MyAPI myAPI = Feign.builder()
        .errorDecoder(new MyErrorDecoder())
        .target(MyAPI.class, "http://localhost:8080");

阅读源码可以知道,错误处理就是处理响应状态码不为200的情况。

// feign.AsyncResponseHandler
void handleResponse(CompletableFuture<Object> resultFuture,
                      String configKey,
                      Response response,
                      Type returnType,
                      long elapsedTime) {
    // 省略部分代码...
    if (Response.class == returnType) {
        // 返回值类型为feign.Response
        if (response.body() == null) {
            resultFuture.complete(response);
        } else if (response.body().length() == null
            || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
            shouldClose = false;
            resultFuture.complete(response);
        } else {
            // Ensure the response body is disconnected
            final byte[] bodyData = Util.toByteArray(response.body().asInputStream());
            resultFuture.complete(response.toBuilder().body(bodyData).build());
        }
    } else if (response.status() >= 200 && response.status() < 300) {
        // 执行成功
        if (isVoidType(returnType)) {
            resultFuture.complete(null);
        } else {
            final Object result = decode(response, returnType);
            shouldClose = closeAfterDecode;
            resultFuture.complete(result);
        }
    } else if (decode404 && response.status() == 404 && !isVoidType(returnType)) {
        // 客户端错误
        final Object result = decode(response, returnType);
        shouldClose = closeAfterDecode;
        resultFuture.complete(result);
    } else {
        // 当响应状态码不为200时会调用错误解码器进行处理
        resultFuture.completeExceptionally(errorDecoder.decode(configKey, response));
    }
    // 省略部分代码...
}

错误解码器的职责就是返回一个指定的异常对象,当返回RetryableException时Feign就会执行重试逻辑。
如下是默认的错误解码器实现:

// feign.codec.ErrorDecoder
public static class Default implements ErrorDecoder {
    // 省略部分代码...
    @Override
    public Exception decode(String methodKey, Response response) {
      FeignException exception = errorStatus(methodKey, response);
      Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER));
      if (retryAfter != null) {
        // 返回RetryableException时Feign框架会执行重试
        return new RetryableException(
            response.status(),
            exception.getMessage(),
            response.request().httpMethod(),
            exception,
            retryAfter,
            response.request());
      }
      // 错误解码器返回异常对象
      return exception;
    }
    // 省略部分代码...
}
重试处理

重试处理是一个非常贴近业务的高级功能,可以根据实际业务需求自定义重试策略。

// 配置自定义重试处理器
MyAPI myAPI = Feign.builder()
        .retryer(new MyRetryer())
        .target(MyAPI.class, "http://localhost:8080");

Feign框架默认的重试处理器对象支持配置重试次数,初始重试时间间隔和最大重试时间间隔。

// feign.Retryer
class Default implements Retryer {
    // 省略部分代码...
    public Default() {
      // 默认的重试处理策略:基础间隔时间为100毫秒,最大间隔时间为1秒,最大重试次数为5次
      this(100, SECONDS.toMillis(1), 5);
    }

    public Default(long period, long maxPeriod, int maxAttempts) {
      this.period = period;            // 基础间隔时间,随着重试次数的增加,间隔时间就是基于该基础间隔时间进行计算的
      this.maxPeriod = maxPeriod;      // 最大间隔时间,每次重试的间隔时间不会超过该值
      this.maxAttempts = maxAttempts;  // 最大重试次数
      this.attempt = 1;
    }
    // 省略部分代码...
}

重试处理是跟错误处理一起配合使用的,当ErrorDecoder返回一个RetryableException异常时就会执行重试逻辑。

静态和默认方法

此用法比较高级,在业务接口中可以定义默认方法和静态方法,详见Static and Default Methods

日志输出

Feign框架支持自定义日志策略,具体来说:
1.指定日志对象
2.设置日志级别

MyAPI myAPI = Feign.builder()
        .logger(new Logger.JavaLogger("ttt").appendToFile("logs/http.log")) // 设置日志对象为java.util.logging.Logger,可以指定日志输出的文件路径(日志文件必须已存在)
        .logger(new Slf4jLogger()) // 使用Slf4j作为日志日志框架,这样就非常灵活,底层的日志组件既可以是log4j,也可以是logback
        .logLevel(Logger.Level.FULL)
        .target(MyAPI.class, "http://localhost:8080");

如果需要在Feign中使用Slf4j作为日志框架时,需要依赖feign-slf4j模块,同时也应该引入底层的日志组件依赖(如:log4j或logback)。

<!-- 在Feign中使用Slf4j作为日志框架 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-slf4j</artifactId>
    <version>11.1</version>
</dependency>
<!-- 使用logback作为底层日志输出组件 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

如果底层日志输出组件使用了log4j或者logback,还应该在项目运行的classpath路径下引入log4j或logback的配置文件。

模板和表达式

Feign支持在业务接口定义中使用表达式,具体来说就是:注解@Param标注的参数可以直接在表达式中引用。
示例如下:

public interface GitHub {
    // 在业务接口定义中使用表达式
    @RequestLine(value = "GET /repos/{owner}/{repo}/contributors", decodeSlash = false)
    @Headers("Accept: {contentType}")
    List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo, @Param("contentType") String contentType);
}

GitHub gitHub = Feign.builder()
        .decoder(new GsonDecoder())
        .target(GitHub.class, "https://api.github.com");
List<Contributor> contributors = gitHub.contributors("OpenFeign", "feign", "application/json");

传递给业务接口的参数将会在表达式中被应用,所以发出的HTTP请求详情如下:

GET https://api.github.com/repos/OpenFeign/feign/contributors HTTP/1.1
Accept: application/json

值得注意的是,Feign只支持在URI Template - RFC 6570中定义的简单字符串表达式。

异步执行

从10.8开始,使用AsyncFeign允许业务方法返回一个CompletableFuture对象异步执行请求。

public interface GitHub {
    // 异步执行
    @RequestLine("GET /repos/{owner}/{repo}/contributors")
    CompletableFuture<List<Contributor>> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

GitHub github = AsyncFeign.asyncBuilder()
                    .decoder(new GsonDecoder())
                    .target(GitHub.class, "https://api.github.com");
// 业务方法返回的是一个CompletableFuture对象
CompletableFuture<List<Contributor>> future = github.contributors("OpenFeign", "feign");
future.thenAccept(contributorList -> { // 执行成功
    for (Contributor contributor : contributorList) {
        System.out.println(contributor.getLogin() + " (" + contributor.getContributions() + ")");
    }
});
future.exceptionally(throwable -> { // 执行异常
    throwable.printStackTrace();
    return null;
});
try {
    // 等待主线程结束
    Thread.sleep(10000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

一个完整的示例

该示例作为Feign框架应用的完整展示。
首先引入对相关组件的依赖:

<!-- 使用Gson编解码 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-gson</artifactId>
    <version>11.1</version>
</dependency>
<!-- 使用OkHttp作为底层通信组件 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
    <version>11.1</version>
</dependency>
<!-- 集成SLF4J日志框架 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-slf4j</artifactId>
    <version>11.1</version>
</dependency>
<!-- 使用logback输出日志 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

业务代码示例:

// 定义业务接口
public interface MyAPI {
    // 不带参数的GET请求
    @RequestLine("GET /test/simple/get")
    String simpleGet();

    // GET请求使用注解@QueryMap动态指定参数,注解@HeaderMap动态指定请求头
    @RequestLine("GET /test/parammap/get")
    String paramMapGet(@QueryMap Map<String, Object> paramMap, @HeaderMap Map<String, String> headerMap);

    // 使用注解@QueryMap标注POJO对象发送动态查询URL参数
    @RequestLine("GET /test/parammap/get")
    String paramPojoGet(@QueryMap Subject subject);

    // POST请求使用注解@Param指定参数名和参数值
    @RequestLine("POST /test/action/post")
    String simplePost(@Param("name") String name, @Param("age") int age);

    // POST请求参数中的对象属性值将会被编码为表单格式传递,使用注解@Headers指定请求头
    @RequestLine("POST /test/simple/post/form")
    @Headers("Content-Type: application/x-www-form-urlencoded;charset=utf-8")
    Object formPost(Subject subject);

    // 使用表达式
    @RequestLine("GET /test/subject/{id}")
    Subject getSubject(@Param("id") Integer id);
}

// 使用业务接口
MyAPI myAPI = Feign.builder()
        .options(new Request.Options(10, TimeUnit.SECONDS, 10, TimeUnit.SECONDS, true))
        .encoder(new GsonEncoder())
        .decoder(new GsonDecoder())
        .logger(new Slf4jLogger())
        .logLevel(Logger.Level.FULL)
        .client(new OkHttpClient())
        .requestInterceptor(new MyRequestInterceptor())
        .target(MyAPI.class, "http://localhost:8080");

在Spring框架中集成Feign

spring-cloud-openfeign项目实现了在Spring框架中集成使用Feign,常常在基于Spring Boot框架的项目中使用。
具体来讲,Spring Cloud OpenFeign为Feign添加了对Spring MVC注解的支持(如:@RequestMapping),在Spring Web中使用HttpMessageConverters等。

在基于Spring Boot框架的项目中使用spring-cloud-openfeign完整示例如下:
第一步:添加依赖配置。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>3.0.2</version>
</dependency>
<!-- 支持将请求参数以表单格式编码 -->
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form</artifactId>
    <version>3.8.0</version>
</dependency>
<!-- 支持将请求参数以json格式编码,同时也支持将响应结果解码为json格式 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-gson</artifactId>
    <version>11.8</version>
</dependency>

第二步:在启动类中使用注解@EnableFeignClients

@SpringBootApplication
@EnableFeignClients // 通过注解方式使用Feign框架实现HTTP请求访问
public class TestOpenfeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestOpenfeignApplication.class, args);
    }
}

第三步:定义并配置业务接口

// 定义业务接口,并配置相应属性
@FeignClient(contextId = "myAPI", name = "myAPI", url = "http://localhost:8080", configuration = MyAPIConfiguration.class)
public interface MyAPI {
    // 访问GET请求
    @RequestMapping(value = "/test/simple/get", method = RequestMethod.GET)
    Object simpleGet();

    // 访问GET请求,PO对象属性会作为URL参数传递
    @RequestMapping(value = "/test/simple/get", method = RequestMethod.GET)
    Object simpleGet(@SpringQueryMap Subject subject);

    // 访问POST请求,参数PO对象属性值将会以表单格式传递
    @RequestMapping(value = "/test/simple/post", method = RequestMethod.POST, headers = "Content-Type=application/x-www-form-urlencoded")
    Object simplePost(Subject subject);

    // 访问POST请求,参数PO对象属性值将会以json格式传递
    @RequestMapping(value = "/test/simple/post/json", method = RequestMethod.POST, headers = "Content-Type=application/json")
    Object jsonPost(Subject subject);
}

// 业务接口配置类
@Configuration(value = "MyAPIConfiguration")
public class MyAPIConfiguration {
   // 定义请求参数编码器
   @Bean
   public Encoder feignEncoder() {
       //return new GsonEncoder();
       // 将请求参数PO对象编码为表单格式
       return new FormEncoder();
   }

   // 定义响应结果解码器
   @Bean
   public Decoder feignDecoder() {
       return new GsonDecoder();
   }

   @Bean
   public Logger logger() {
       return new Slf4jLogger();
   }

   @Bean
   public Logger.Level logLevel() {
       return Logger.Level.FULL;
   }
}

第四步:调用接口

@Test
public void testSimpleGet() {
    // 访问简单GET请求
    Object result = this.myAPI.simpleGet();
    Assertions.assertNotNull(result);
}

@Test
public void testSimpleGetParam() {
    Subject subject = Subject.builder()
            .id(10)
            .name("TEST")
            .build();
    // 访问GET请求,带URL参数
    Object result = this.myAPI.simpleGet(subject);
    Assertions.assertNotNull(result);
}

@Test
public void testSimplePost() {
    Subject subject = Subject.builder()
            .id(10)
            .name("TEST")
            .build();
    // 访问POST请求,参数以表单格式传递
    Object result = this.myAPI.simplePost(subject);
    Assertions.assertNotNull(result);
}

@Test
public void testJsonPost() {
    Subject subject = Subject.builder()
            .id(10)
            .name("zhangsan")
            .build();
    // 访问POST请求,参数以json格式传递
    Object result = this.myAPI.jsonPost(subject);
    Assertions.assertNotNull(result);
}

从上面的示例可以看到,在使用Spring Cloud OpenFeign框架作为HTTP客户端时非常方便,但是要注意以下几点:
(1)如果想实现表单方式传递参数,一定要使用表单编码器(FormEncoder),同时必须在注解@RequestMapping中明确设置请求消息头:headers = "Content-Type=application/x-www-form-urlencoded"
(2)如果想实现json格式传递参数,可以不用明确指定JSON格式编码器,但是必须在注解@RequestMapping中设置消息头:headers = "Content-Type=application/json";如果不设置该消息头,默认是按照表单格式传递的。
(3)如果想传递查询URL参数,不能使用Feign框架自身的注解@QueryMap,只能使用Spring Cloud OpenFeign提供的注解@SpringQueryMap

另外,如果在一个项目中需要针对不同的服务定义多个FeignClient,且不同的FeignClient需要不同的配置(如:不同的编码器,解码器,请求拦截器等),使用configuration属性进行配置。
可以自定义配置的属性详见Overriding Feign Defaults

// 客户端1
@FeignClient(value = "FooClient", url = "http://foo-server.com/services", configuration = FooClientConfig.class)
public interface FooClient {
    // 省略部分代码...
}

// 自定义配置类,一定不能使用如@Component这样的任何Spring Bean注解,否则会报错Bean定义已存在
public class FooClientConfig {
    @Bean
    public Encoder feignEncoder() {
        //return new GsonEncoder();
        // 将请求参数编码为表单格式
        return new FormEncoder();
    }

    @Bean
    public Decoder feignDecoder() {
        return new GsonDecoder();
    }

    @Bean
    public Logger feignLogger() {
        return new Slf4jLogger();
    }

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

// 客户端2
@FeignClient(value = "BarClient", url = "http://bar-server.com/services", configuration = BarClientConfig.class)
public interface BarClient {
    // 省略部分代码...
}

// 自定义配置类,一定不能使用如@Component这样的任何Spring Bean注解,否则会报错Bean定义已存在
public class BarClientConfig {
    @Bean
    public Encoder feignEncoder() {
        return new GsonEncoder();
    }

    @Bean
    public Decoder feignDecoder() {
        return new GsonDecoder();
    }

    @Bean
    public Logger feignLogger() {
        return new Slf4jLogger();
    }

    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

【参考】
https://www.cnblogs.com/syui-terra/p/14386188.html Feign 动态URL 解决记录
https://blog.csdn.net/kysmkj/article/details/89672952 Feign 访问远程api,动态指定url
https://stackoverflow.com/questions/35803093/how-to-post-form-url-encoded-data-with-spring-cloud-feign How to POST form-url-encoded data with Spring Cloud Feign
https://cloud.spring.io/spring-cloud-openfeign/reference/html/ Spring Cloud OpenFeign
https://segmentfault.com/a/1190000039889836 Spring Cloud OpenFeign入门和实战
https://www.jianshu.com/p/e319a7a550a2 Spring Cloud Feign 分析(二)之FeignClient注解实现版本兼容
https://blog.51cto.com/u_3631118/3153818 [享学Feign] 一、原生Feign初体验,Netflix Feign or Open Feign?
https://blog.csdn.net/xiao_cai_ming/article/details/108168342 @FeignClient的使用及与Spring Boot的版本适配
https://github.com/OpenFeign/feign-form
https://www.itmuch.com/spring-cloud-sum/feign-form-params/ 使用Feign实现Form表单提交
https://www.cnblogs.com/huahua035/p/9100283.html Feign Form表单POST提交
https://www.zybuluo.com/JunQiu/note/1291674 HTTP POST body常见的四种数据格式
https://cloud.tencent.com/developer/article/1588517 [享学Feign] 十一、Feign通过feign-slf4j模块整合logback记录日志
https://hackernoon.com/setting-up-multiple-configurations-for-feign-clients-a-step-by-step-guide-f51y3yb3 为多个FeignClient组件使用不同的配置
https://www.cnblogs.com/admol/p/14040980.html SpringCloud 实战:引入Feign组件,发起服务间调用

posted @ 2021-08-20 19:27  nuccch  阅读(2255)  评论(0编辑  收藏  举报