深入理解feign(01)-使用入门

前言

feign的使用文档中,开篇第一句就是Feign makes writing java http clients easier,中文译为Feign使得写java http客户端更容易。言下之意,Feign其实是一个java http客户端的类库。

本文我们将对Feign做一个大概的了解,并对其基本用法进行掌握,后续章节我们将深入Feign的各种应用场景及源码,让我们不仅知其然,还要知其所以然。

为什么选择Feign

如果读者有用过JerseySpringCXF来实现web服务端,那么一定会对他们通过注解方法来定义接口的方式大开眼界。那么,我们为什么选择Feign,它又有哪些优势呢?

  1. Feign允许我们通过注解的方式实现http客户端的功能,给了我们除了Apache HttpComponents之外的另一种选择
  2. Feign能用最小的性能开销,让我们调用web服务器上基于文本的接口。同时允许我们自定义编码器解码器错误处理器等等

Feign如何工作的呢

Feign通过注解和模板的方式来定义其工作方式,参数(包括url、method、request和response等)非常直观地融入到了模板中。尽管Feign设计成了只支持基于文本的接口,但正是它的这种局限降低了实现的复杂性。而我们写http客户端代码的时候,超过90%的场景是基于文本的接口调用。另一个方面,使用Feign还可以简化我们的单元测试。

基本用法

典型的用法如下所示。

public interface UserService {
    @RequestLine("GET /user/get?id={id}")
    User get(@Param("id") Long id);
}

public class User {
    Long id;
    String name;
}

public class Main {
    public static void main(String[] args) {
        UserService userService = Feign.builder()
            .options(new Request.Options(1000, 3500))
            .retryer(new Retryer.Default(5000, 5000, 3))
            .target(UserService.class, "http://api.server.com");
        System.out.println("user: " + userService.get(1L));
    }
}

Feign下面接口的注解

Feign通过注解和模板的方式来定义契约,那么又有哪些注解,分别是做什么用的呢?下面的表格参考了Feign官网的Annotation,给出了基本用法。

Annotation Interface Target Usage
@RequestLine Method 用于定义method和uri模板,其值由@Param传入。
@Param Parameter 模板变量,它的值将被用于替换表达式。
@Headers Method, Type 用于定义header模板,其值由@Param传入。该注解可声明在Type上,也可声明在Method上。当声明在Type上时,相当于其下面的所有Method都声明了。当声明在Method上时,仅对当前Method有效。
@QueryMap Parameter 可定义成一个key-value的Map,也可以定义成POJO,用以扩展进查询字符串。
@HeaderMap Parameter 可定义成一个key-value的Map,用于扩展请求头。
@Body Method 用于定义body模板,其值由@Param传入。

模板和表达式

模板和表达式模式,是基于URI Template - RFC 6570来实现的。表达式通过在方法上@Param修饰的参数来填充。

表达式必须以{}来包装变量名。也可使用正则表达式来验证,变量名+:+正则表达式的方式。表达式定义如下所示:

  1. {name}
  2. {name:[a-zA-Z]*}

可以运用表达式的地方有下面几处。

  1. @RequestLine
  2. @QueryMap
  3. @Headers
  4. @HeaderMap
  5. @Body

他们将遵循URI Template - RFC 6570规约。

  1. 未正确匹配的表达式将被忽略(忽略的意思就是,该变量在表达式中将被设置为null)
  2. 表达式值设置之前不会通过Encoder进行编码
  3. @Body使用的时候必须在Header里通过Content-Type指明内容类型

@Paramexpander属性,该属性为Class类型,可以通过编码的方式更灵活地进行转换。如果返回的结果为null或空字符串,表达式将被忽略。

public interface Expander {
    String expand(Object value);
}

另外,@Param可同时运用到多处,如下所示:

public interface ContentService {
  @RequestLine("GET /api/documents/{contentType}")
  @Headers("Accept {contentType}")
  String getDocumentByType(@Param("contentType") String type);
}

Feign的自定义设置

可以通过Feign.builder()来自定义设置一些拦截器,用于增强其语义。比如我们可以增加超时拦截器、编码拦截器、解码拦截器、重试拦截器等等。如下所示:

interface Bank {
  @RequestLine("POST /account/{id}")
  Account getAccountInfo(@Param("id") String id);
}

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

Feign集成第三方组件

可以和很容易地和第三方组件结合使用,扩展了其功能,也增加了其灵活性。我们可以查阅官网文档,链接地址:integrations

1. Gson

通过encoder和decoder来使用

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

2. Jackson

Gson一样,也是通过encoder和decoder来使用

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

3. JAXB

Gson一样,也是通过encoder和decoder来使用

public class Example {
  public static void main(String[] args) {
    Api api = Feign.builder()
             .encoder(new JAXBEncoder())
             .decoder(new JAXBDecoder())
             .target(Api.class, "https://apihost");
  }
}

4. JAX-RS

JAX-RS定义了自己的一套注解,我们可以通过和JAX-RS注解的集成来定义我们自己的注解皮肤。该功能需要结合contract使用。

interface GitHub {
  @GET @Path("/repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
}

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

5. OkHttp

OkHttp是一个http客户端类库,我们也可以将其包装成Feign的形式。该功能需要结合client来使用。

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

6. Ribbon

Ribbon提供了客户端负载均衡功能。我们也可以和其一起集成使用。

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

7. Hystrix

Hystrix是一个断路器组件,为了保证分布式系统的健壮性,在某一些服务不可用的情况下,可避免出现雪崩效应。也可以和Feign结合使用

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

8. SOAP

SOAP是基于http之上的一种协议,其通信方式使用的是xml格式。

public class Example {
  public static void main(String[] args) {
    Api api = Feign.builder()
         .encoder(new SOAPEncoder(jaxbFactory))
         .decoder(new SOAPDecoder(jaxbFactory))
         .errorDecoder(new SOAPErrorDecoder())
         .target(MyApi.class, "http://api");
  }
}

9. SLF4J

slf4j是一个日志门面,给各种日志框架提供了统一的入口。

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

Decoders

当我们的接口返回类型不为feign.ResponseStringbyte[]void时,我们必须定义一个非默认的解码器。以Gson为例

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

当我们想在feign.Response进行解码之前做一些事情,我们可以通过mapAndDecode来自定义。

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

当我们定义的接口method为POST,且传入的类型不为String或者byte[],我们需要自定义编码器。同时需要在header上指明Content-Type

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"));
  }
}

扩展功能

1. 基本使用

接口的定义可以是单一的接口,也可以是带继承层级的接口列表。

interface BaseAPI {
  @RequestLine("GET /health")
  String health();

  @RequestLine("GET /all")
  List<Entity> all();
}
interface CustomAPI extends BaseAPI {
  @RequestLine("GET /custom")
  String custom();
}

我们也可以定义泛型类型

@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> { }

2. 日志级别

Feign会根据不同的日志级别,来输出不同的日志,在Feign里面定义了4种日志级别。

/**
 * Controls the level of logging.
 */
public enum Level {
  /**
   * No logging.不记录日志
   */
  NONE,
  /**
   * Log only the request method and URL and the response status code and execution time.
   * 仅仅记录请求方法、url、返回状态码及执行时间
   */
  BASIC,
  /**
   * Log the basic information along with request and response headers.
   * 在记录基本信息上,额外记录请求和返回的头信息
   */
  HEADERS,
  /**
   * Log the headers, body, and metadata for both requests and responses.
   * 记录全量的信息,包括:头信息、body信息、请求和返回的元数据等
   */
  FULL
}

使用方式如下:

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

3. 请求拦截器

我们可以通过定义一个请求拦截器RequestInterceptor来对请求数据进行修改,比如添加一个请求头或者校验授权信息等等。

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");
  }
}

4. 动态查询参数@QueryMap

一般情况下,我们使用@QueryMap时,传入的参数为Map<String, Object>类型,如下所示:

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

但有时候,为了让我们的参数定义得更清晰易懂,我们也可以使用POJO方式,如下所示。这种方式是通过反射直接获取字段名称和值的方式来实现的。如果POJO里面的某个字段为null或者空串,将会从查询参数中移除掉(也就是不生效)。

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

如果我们更喜欢使用gettersetter的方式来读取和设置值,那么我们可以自定义查询参数编码器。

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

5. 自定义错误处理器

Feign有默认的错误处理器,当我们想自行处理错误,也是可以的。可以通过自定义ErrorDecoder来实现。

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

它会捕获http返回状态码为非2xx的错误,并调用ErrorDecoder. decode()方法。我们可以抛出自定义异常,或者做额外的处理逻辑。如果我们想重复多次调用,需要抛出RetryableException,并定义且注册额外的Retryer

6. 自定义Retry

我们可以通过实现Retryer接口的方式来自定义重试策略。Retry会对IOExceptionErrorDecoder组件抛出的RetryableException进行重试。如果达到了最大重试次数仍不成功,我们可以抛出RetryException

自定义Retryer的使用如下所示:

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

7. 接口的静态方法和默认方法

在java8及以上版本,我们可以在接口里面定义静态方法和默认方法。Feign也支持这种写法,但是有特殊的作用。

  1. 静态方法可以写自定义的Feign定义
  2. 默认方法可以在参数中传入默认值
interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

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

总结

本文先简单介绍了Feign,然后给出了一个入门级的例子,最后对每个功能、组件和扩展进行了补充说明。楼主相信通过这些文字,足够让我们进入Feign的大门了。

后面我们将更加深入地了解Feign,尤其是Feign的源码。

参考链接

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,355评论 6 343
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,472评论 2 59
  • 今天下午大课间,我们不出去,戴老师想正好给九个小组的组长开个会。 之后戴老师就叫了我们去了三楼的...
    小王子WXN阅读 498评论 1 4
  • 中国人的餐桌文化源远流长。人与人之间联络感情最好的方式之一就是在餐桌上。 平时节假日约个餐厅聚一聚,朋友聚会约个餐...
    娜娜遇到的那些人那些事阅读 839评论 2 3