feign 官方文档翻译整理

feign.png

Ribbon
RibbonClient 重写了 Feign 对 URL 的解析逻辑,实现了智能路由、负载均衡和重试机制。
引入 feign-ribbon 包,用 Feign client 的名字代替 API 的域名/IP+Port :

public class Example {
  public static void main(String[] args) {
    MyService api = Feign.builder()
          .client(RibbonClient.create())
          .target(MyService.class, "https://myAppProd");
  }
}

学习教程

Hystrix
HystrixFeign 实现了断路器功能。
要使用断路器功能,要引入 feign-hystrix 包。

public class Example {
  public static void main(String[] args) {
    MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd");
  }
}

SLF4J
SLF4JModule 支持将 Feign 的日志写入 SLF4J(Logback, Log4J 等)。
要使用该功能,要引入 feign-slf4j 包和 Logback/Log4J/ 包。

public class Example {
  public static void main(String[] args) {
    GitHub github = Feign.builder()
                     .logger(new Slf4jLogger())
                     .logLevel(Level.FULL)
                     .target(GitHub.class, "https://api.github.com");
  }
}

Decoders
如果 Interface 中的 method 返回的类型不是 Response、String、byte[]、void,那么就需要自定义 Decoder 了,因为默认的 Decoder 仅支持这几种类型的解码(默认 Decoder 在 feign.codec 包下)。

解析 JSON 的 Decoder 配置示例(使用 feign-gson 扩展包):

public class Example {
  public static void main(String[] args) {
    GitHub github = Feign.builder()
                     .decoder(new GsonDecoder())
                     .target(GitHub.class, "https://api.github.com");
  }
}

在某些场景中,Decoder 对响应内容做解码之前需要对响应内容先做一些处理,Feign.Builder 提供了 mapAndDecode 函数支持这类场景。比如要从一个响应内容是 HTML 网页或者 XML 的 API 中抽取出有用的信息,组装成 JSON 格式的字符串,然后再使用 GonDecoder 将 JSON 字符串解码成业务对象类型;再比如目标 API 的响应内容是 jsonp (一种 json 使用模式)格式的,GsonDecoder 是无法直接做解码的,这时就要对 API 的响应内容先做一次处理,然后再用 GsonDecoder 对处理后的内容做解码:

public class Example {
  public static void main(String[] args) {
    JsonpApi jsonpApi = Feign.builder()
                         .mapAndDecode((response, type) -> jsopUnwrap(response, type), new GsonDecoder())
                         .target(JsonpApi.class, "https://some-jsonp-api.com");
  }
}

Encoders
定义一个参数类型是 String 或者 byte[] 的 method,向 POST 接口发送 request body :

interface LoginClient {
  @RequestLine("POST /")
  @Headers("Content-Type: application/json")
  void login(String content);
}

public class Example {
  public static void main(String[] args) {
    client.login("{\"user_name\": \"denominator\", \"password\": \"secret\"}");
  }
}

上面的代码不安全,也不易读,更好的做法是将 method 参数声明成自定义类型,并给 Feign client 指定 Encoder:

static class Credentials {
  final String user_name;
  final String password;

  Credentials(String user_name, String password) {
    this.user_name = user_name;
    this.password = password;
  }
}

interface LoginClient {
  @RequestLine("POST /")
  void login(Credentials creds);
}

public class Example {
  public static void main(String[] args) {
    LoginClient client = Feign.builder()
                              .encoder(new GsonEncoder())
                              .target(LoginClient.class, "https://foo.com");

    client.login(new Credentials("denominator", "secret"));
  }
}

@Body

@Headers
使用注解添加请求头
固定的键值对放到 Interface 或者 method 上:

@Headers("Accept: application/json")
interface BaseApi<V> {
  @Headers("Content-Type: application/json")
  @RequestLine("PUT /api/{key}")
  void put(@Param("key") String key, V value);
}

动态键值对可以使用变量表达式:

public interface Api {
   @RequestLine("POST /")
   @Headers("X-Ping: {token}")
   void post(@Param("token") String token);
}

某些场景下请求头中的键值对都是动态的,这时可以将 method 参数声明成 Map 类型,使用 @HeaderMap 实现动态请求头拼装:

public interface Api {
   @RequestLine("POST /")
   void post(@HeaderMap Map<String, Object> headerMap);
}

给所有的 target 请求统一添加 Header

要给所有的 target 的 Request 统一添加请求头,需要自定义实现 RequestInterceptor(要保证线程安全),RequestInterceptor 会作用到 target 的所有 method 。

如果要给每一个 target 甚至是每一个的 method 定制独特的请求头,那就要自定义 Target 了,因为 RequestInterceptor 无法访问 method 的元数据。如果用前面介绍的 @Header @Param 的方式实现定制化就会有大量重复代码,甚至无法达到用户的目的。

使用 RequestInterceptor 设置请求头的样例详见 Request Interceptors 章节。

自定义 Target 设置定制化请求头:

  static class DynamicAuthTokenTarget<T> implements Target<T> {
    public DynamicAuthTokenTarget(Class<T> clazz,
                                  UrlAndTokenProvider provider,
                                  ThreadLocal<String> requestIdProvider);

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

  public class Example {
    public static void main(String[] args) {
      Bank bank = Feign.builder()
              .target(new DynamicAuthTokenTarget(Bank.class, provider, requestIdProvider));
    }
  }

示例中的 provider 和 requestIdProvider 都是 ThreadLocal 类型的,示例演示了如何给 Bank 这个 Interface (即 feign client)中的 method 添加认证请求头和请求标识(常用于实现调用链路跟踪)。

要给所有的 feign client 统一添加请求头,得自定义 RequestInterceptor;
只给某个 feign client 或者多个 feign client 中的 具有共性的 method 添加请求头,得自定义 Target ,在构建 feign client 时使用 Feign.builder().target(new DynamicAuthTokenTarget(Bank.class, provider, requestIdProvider)); 给指定的 feign client 添加请求头处理逻辑。
解释:具有共性的 method 在代码中随处可见,对多个类中具有共性的 method 的统一处理的常用思路是切面编程 AOP 、拦截器等,在 feign 中,利用 Target 处理多个 feign client 中具有共性的 method 的方式也是这种思路。

高级用法
Basics API
feign 支持接口继承,公共部分可以放到父接口:

@Headers("Accept: application/json")
interface BaseApi<V> {

  @RequestLine("GET /api/{key}")
  V get(@Param("key") String key);

  @RequestLine("GET /api")
  List<V> list();

  @Headers("Content-Type: application/json")
  @RequestLine("PUT /api/{key}")
  void put(@Param("key") String key, V value);
}

interface FooApi extends BaseApi<Foo> { }

interface BarApi extends BaseApi<Bar> { }

Logging
Feign 提供的记录请求响应日志的简单实现:

public class Example {
  public static void main(String[] args) {
    GitHub github = Feign.builder()
                     .decoder(new GsonDecoder())
                     .logger(new Logger.JavaLogger("GitHub.Logger").appendToFile("logs/http.log"))
                     .logLevel(Logger.Level.FULL)
                     .target(GitHub.class, "https://api.github.com");
  }
}

Request Interceptors
RequstInterceptor 常用场景之一
给请求头添加代理记录追踪 X-Forwarded-For:

static class ForwardedForInterceptor implements RequestInterceptor {
  @Override public void apply(RequestTemplate template) {
    template.header("X-Forwarded-For", "origin.host.com");
  }
}

public class Example {
  public static void main(String[] args) {
    Bank bank = Feign.builder()
                 .decoder(accountDecoder)
                 .requestInterceptor(new ForwardedForInterceptor())
                 .target(Bank.class, "https://api.examplebank.com");
  }
}

RequstInterceptor 常用场景之二
给请求头添加认证身份,Feign 提供了 BasicAuthRequestInterceptor:

public class Example {
  public static void main(String[] args) {
    Bank bank = Feign.builder()
                 .decoder(accountDecoder)
                 .requestInterceptor(new BasicAuthRequestInterceptor(username, password))
                 .target(Bank.class, "https://api.examplebank.com");
  }
}

扩展@Param
被 @Param 标记的参数,得到的值是参数类型的 toString() 决定的。要改变这个默认的行为,可以给 @Param 指定一个自定义的 expander :

public interface Api {
  @RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
}

动态查询参数
用 @QueryMap 注解 Map 对象 构建 Request 参数:

public interface Api {
  @RequestLine("GET /find")
  V find(@QueryMap Map<String, Object> queryMap);
}

用 @QueryMap 注解 POJO 对象 构建 Request 参数:

public interface Api {
  @RequestLine("GET /find")
  V find(@QueryMap CustomPojo customPojo);
}

如果没有指定 QueryMapEncoder ,那么请求参数的 name 就是 POJO 对象的属性名。如果 POJO 属性值是 null ,那么该属性会被忽略。下面的 POJO 作为 method 参数的话,那么生成 URL 就是这样的 “/find?name={name}&number={number}”:

public class CustomPojo {
  private final String name;
  private final int number;

  public CustomPojo (String name, int number) {
    this.name = name;
    this.number = number;
  }
}

可以自定义一个 QueryMapEncoder ,比如用来改变属性名的样式(驼峰转下划线):

public class Example {
  public static void main(String[] args) {
    MyApi myApi = Feign.builder()
                 .queryMapEncoder(new MyCustomQueryMapEncoder())
                 .target(MyApi.class, "https://api.hostname.com");
  }
}

不自定义 QueryMapEncoder 的情况下,默认的 Encoder 会使用反射处理 POJO 的属性。如果不习惯用构造器定义 POJO,而习惯使用 Getter 和 Setter 方式构建查询参数,那么可以使用 BeanQueryMapEncoder:

public class Example {
  public static void main(String[] args) {
    MyApi myApi = Feign.builder()
                 .queryMapEncoder(new BeanQueryMapEncoder())
                 .target(MyApi.class, "https://api.hostname.com");
  }
}

Error Handling
HTTP 状态码不是 2xx 的响应都会触发 ErrorDecoder 中的 decode 方法,将异常响应包装成自定义异常是常见场景。如果 decode 方法返回的是 RetryableException ,将会触发请求重试,Retryer 负责重试。

Retry
默认情况下,只要 Feign 收到了 IOException ,就会触发重试,当 ErrorDecoder 抛出了 RetryableException 时也会触发请求重试。要改变这个默认行为,需要自定义一个 Retryer 实现类。

public class Example {
  public static void main(String[] args) {
    MyApi myApi = Feign.builder()
                 .retryer(new MyRetryer())
                 .target(MyApi.class, "https://api.hostname.com");
  }
}

Retryer 中的 continueOrPropagate(RetryableException e) 方法返回 true 就触发重试,返回 false 则不触发。
重试后仍然失败则会抛出 RetryException 。可以利用 exceptionPropagationPolicy() 抛出重试失败的根源。

Metrics
Static and Default Methods
Interface 中不仅能声明 API 方法,还可以定义 static 方法和 default 方法(JDK 1.8+)。static 方法可以定义 feign client 的公共配置;default 方法可以用来组装查询对象、定义 API 方法的默认参数值。

Interface GitHub {
  // API 方法一
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

  // API 方法二
  @RequestLine("GET /users/{username}/repos?sort={sort}")
  List<Repo> repos(@Param("username") String owner, @Param("sort") String sort);

  default List<Repo> repos(String owner) {
    return repos(owner, "full_name");
  }

  /**
   * Lists all contributors for all repos owned by a user.
   */
  default List<Contributor> contributors(String user) {
    MergingContributorList contributors = new MergingContributorList();
    for(Repo repo : this.repos(owner)) {
      contributors.addAll(this.contributors(user, repo.getName()));
    }
    return contributors.mergeResult();
  }

  static GitHub connect() {
    return Feign.builder()
                .decoder(new GsonDecoder())
                .target(GitHub.class, "https://api.github.com");
  }

  public static void main(String[] args) {
    // 是这样写,还是 .repos("Mike", null) 这样写?
    // 调用 API 方法二,该方法有 2 个参数,只传了第一参数,第 2 个参数是空值,feign 会调用 default repos 方法自动填充参数
    List<Repo> allReposOfMike = GitHub.connect().repos("Mike");
  }
}

通过 CompletableFuture支持异步请求
Feign 10.8 中新增了 AsyncFeign ,允许 Interface 中的 API 方法返回 CompletableFuture 类型的实例。

Interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  CompletableFuture<List<Contributor>> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

public class MyApp {
  public static void main(String... args) {
    GitHub github = AsyncFeign.asyncBuilder()
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");

    // Fetch and print a list of the contributors to this library.
    CompletableFuture<List<Contributor>> contributors = github.contributors("OpenFeign", "feign");
    for (Contributor contributor : contributors.get(1, TimeUnit.SECONDS)) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

Feign 提供了 2 个异步 client 实现:

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

推荐阅读更多精彩内容