Juconcurrent 学而不思则罔,思而不学则殆。

深入理解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的源码。

参考链接

  • https://github.com/OpenFeign/feign
  • https://www.jianshu.com/p/3d597e9d2d67
  • https://my.oschina.net/joryqiao/blog/1925633

Content