《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 jianshu.com/p/b6a47b06d3dc 「木子小三金」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

Fegin是一个java调用HTTP的客户端binder,其灵感来自于RetrofitJAXRS-2.0WebSocket。Feign的首要目标是降低将Denominator统一的绑定到HTTP的复杂性,而无需关心是否为RESTfull.

Why Feign and not X?

Feign的使用方式与Jersey和CXF编写java client来调用REST或SOAP服务类似。此外,Feign还允许编写基于Apache HC之类的类库的代码。Feign通过自定义decoder和error处理以最小的成本将你的代码连接到http,并可以使用到任何基于文本的http API上。

Feign如何工作?

Feign基于将注解转换成请求模板的方式工作。参数会简单直接的应用到模板上。尽管Feign只支持基于文本的API,但是它的简化了类似于请求重放等系统层面的开发。此外,Feign让单元测试更加方便。

Java 版本兼容

Feign 10.x版本基于java 1.8同时可以在java9,10和11版本上工作。如果要在JDK6上使用,请使用Feign 9.x

基本使用方式

下面是一个典型的用法,适配Retrofit示例。

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

@RequestLine("POST /repos/{owner}/{repo}/issues")
void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);

}

public static class Contributor {
String login;
int contributions;
}

public static class Issue {
String title;
String body;
List<String> assignees;
int milestone;
List<String> labels;
}

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

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

接口注解

Feign的注解定义了接口及其client如何工作的 Contract,Feign默认Contract 如下表。

Annotation Interface Target Usage
@RequestLine Method 用来定义request 的 HttpMethodUriTemplate。表达式(expression)使用大括号{}括起来,表达式的值会由被@Param注解的参数提供。
@Param Parameter 定义模板变量,变量的值会根据参数名解析到Expression内。
@Headers Method或Interface 定义HeaderTemplate,表达式的值会由被@Param注解的参数提供。@Header可以使用到MethodInterface上,当作用到Interface上时Interface内的所有方法都会使用该Header,如果作用到Method上那只会对该方法使用Header
@QueryMap Parameter 可以修饰K-V结构的Map或POJO,用以扩展查询参数。
@HeaderMap Parameter 可以修饰K-V结构的Map或POJO,用以扩展Http Header
@Body Method UriTemplateHeaderTemplate类似,用来定义一个Template,表达式的值会由被@Param注解的参数提供。

表达式(Templates)与模板(Expressions)

Feign的Expressions遵循URI Template - RFC 6570 level 1规范。Expressions需要配合Param所修饰的方法参数进行扩展。

Example

public interface GitHub {

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

class Contributor {
String login;
int contributions;
}
}

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

/* The owner and repository parameters will be used to expand the owner and repo expressions
* defined in the RequestLine.
*
* the resulting uri will be https://api.github.com/repos/OpenFeign/feign/contributors
*/
github.contributors("OpenFeign", "feign");
}
}

  • 表达式(Expression):被花括号括起来的内容,可以使用正则表达式,表达式名称与正则表达式用冒号:分隔,比如{owner:[a-zA-Z]*}。表达式的值由被@Param修饰的变量提供。
  • 模板(Template):多个表达式的集合,如GET /repos/{owner}/{repo}/contributors

请求参数说明

RequestLineQueryMap 的模板遵循URI Template - RFC 6570 level 1规范,具体的规范如下:

  • 忽略掉无法解析的表达式
  • 如果没有对字符串或变量进行编码或未通过@Param注解期编码方式,默认会使用pct-encoded编码。

未定义的值和空值

未定义的表达式也是合法的表达式,如果明确指定表达式的值为Null或者为空,遵循URI Template - RFC 6570规范,则会提供一个空的值给表达式。 当Feign解析表达式时,会优先判断是否定义了表达式的值来决定是否要保留查询参数名。如果未定义表达式的值,查询参数名将会被去除,看下面的示例。

Empty String

public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("param", "");
this.demoClient.test(parameters);
}

Result

http://localhost:8080/test?param=

Missing

public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
this.demoClient.test(parameters);
}

Result

http://localhost:8080/test

Undefined

public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("param", null);
this.demoClient.test(parameters);
}

Result

http://localhost:8080/test

高级用法 提供了更多的示例.

关于斜杠/

默认情况下@RequestLine@QueryMap 模板不会对/编码,如果要对其编码可以设置@RequestLinedecodeSlash值为false

关于加号 +

根据URI规范,请求地址和请求参数是允许使用加号+的,但是在查询的处理上可能会不一致。在一些旧的系统上+可能会被当作空白字符。Feign根据现代系统的做法不会将+当作空白字符,而是编码为%2B

如果要将+处理成空白字符,可以使用空格或者直接将其编码为%20

自定义扩展

@Param可以通过可选属性expander对某一参数进行扩展处理。expander属性需要接收一个实现了Expander接口的实现类。

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

方法的返回值规则与上述规则相同。忽略掉无法解析的表达式; 如果没有对字符串或变量进行编码或未通过@Param注解期编码方式,默认会使用pct-encoded编码。更多使用示例参考Custom @Param Expansion

请求头扩展

HeadersHeaderMap 模板规则与Request Parameter Expansion 规则相同

  • 如果返回值为null或空,则移除整个header
  • 如果未进行pct编码,则会进行pct编码

Headers 示例代码.

请注意@Param 参数和参数名称:

如果@RequestLine, @QueryMap, @BodyTemplate, 或 @Headers拥有相同的表达式名称,那么这些表达式会被赋予相同的值。如下示例中的contentType将会被同时解析到@RequestLine@Headers的表达式中

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

在设计接口时需要额外注意。

请求体扩展

Body模板的规则与Request Parameter Expansion 相同:

  • 忽略掉无法解析的表达式
  • 扩展内容在被传递到请求体前不会被编码
  • 必须在header内指定Content-Type。参考示例代码 Body Templates

定制化服务

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支持创建多个api接口。通过继承接口Target(默认实现为HardCodedTarget)实现,在程序运行request前会动态的发现并包装请求。

例如,下面的场景对当前使用的url增加CloudIdentityTarget进行auth验证。

public class CloudService {
public static void main(String[] args) {
CloudDNS cloudDNS = Feign.builder()
.target(new CloudIdentityTarget<CloudDNS>(user, apiKey));
}

class CloudIdentityTarget extends Target<CloudDNS> {
/* implementation of a Target */
}
}

更多示例

Feign包含了GitHubWikipedia的客户端示例代码,以供大家作为开发参考。特殊的示例请参考example daemon.


集成能力

Feign设计原则就是能够与其它开源工具友好的工作,也欢迎将您喜欢的项目集成进来。

Gson

Gson 包含了可以与JSON API共同使用的编码器和解码器

使用Feign.Builder添加GsonEncoderGsonDecoder

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

Jackson

Jackson包含了可以与JSON API共同使用的编码器和解码器

使用Feign.Builder添加JacksonEncoderJacksonDecoder

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

Sax

SaxDecoder 可以在标准的JVM或Android环境下解码XML。

使用Sax对响应数据进行解析

public class Example {
public static void main(String[] args) {
Api api = Feign.builder()
.decoder(SAXDecoder.builder()
.registerContentHandler(UserIdHandler.class)
.build())
.target(Api.class, "https://apihost");
}
}

JAXB

JAXB包含了可以与XML API共同使用的编码器和解码器

使用Feign.Builder添加JAXBEncoderJAXBDecoder

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

JAX-RS

JAXRSContract对标准的JAX-RS进行了重新处理,目前基于1.1规范。

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

OkHttp

使用OkHttpClient直接将Feign请求交由OkHttp处理,OkHttp使用了SPDY协议,可以更好的对网络进行控制

Feign使用OkHttp只需要将OkHttp加入到classpath,然后配置Feign使用OkHttpClient。

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

Ribbon

RibbonClient为Feign client重写URL解析,同时提供了智能路由和弹性能力。

集成ribbon需要将url改为ribbon的客户端名称,如myAppProd

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

Java 11 Http2

Http2Client直接将Feign http请求交由Java11 New HTTP/2 Client处理以实现HTTP/2。

若为Feign使用New HTTP/2客户端需要JDK11,然后配置Feign使用Http2Client。

GitHub github = Feign.builder()
.client(new Http2Client())
.target(GitHub.class, "https://api.github.com");

Hystrix

HystrixFeign使用Hystrix提供了断路器的支持。

若要使用Hystrix,需要将Hystrix模块加入classpath。然后使用HystrixFeign builder。

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

SOAP

SOAP包含了可以与XML API共同使用的编码器和解码器

该模块通过JAXB和SOAPMessage对SOAP Body提供编码和解码。同时将SOAPFault解码能力包装到了原生的javax.xml.ws.soap.SOAPFaultException,所以使用时只需要捕获SOAPFaultException来处理SOAPFault。

Add SOAPEncoder and/or SOAPDecoder to your Feign.Builder like so:

使用Feign.Builder来添加SOAPEncoderSOAPDecoder

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

注意:如果SOAP的错误响应包含http code (4xx、5xx、…),则需要添加SOAPErrorDecoder对错误进行处理。

SLF4J

SLF4JModule直接将Feign's logging交由SLF4J处理,你可以方便的选择logging的后端实现。

若要使用SLF4J,需要添加SLF4J和对应的后端模块到classpath内,然后使用Feign.builder配置logging。

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.builder()可以指定Decoder对响应数据进行解码。

如果接口内某些方法反回了除Response, String, byte[]void之外的返回值,则需要配置一个自定义的 Decoder

使用gson对json进行解码

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

如果你需要在解码器解码之前作一些前置操作,可以使用mapAndDecode。 举个例子,现在要求API只处理jsonp,可以在传输之前不对jsonp进行包装。

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

The simplest way to send a request body to a server is to define a POST method that has a String or byte[] parameter without any annotations on it. You will likely need to add a Content-Type header.

一个最简单的请求方式就是使用POST方法传递一个Stringbyte[]类型参数到服务端,有时可能需要在header内指定Content-Type,如下方式

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

在这里可以使用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 模板

@Body注解用来声明一个请求体模板,并配合@Param注解对请求参数进行扩展。在使用时需要指定Content-Type

interface LoginClient {

@RequestLine("POST /")
@Headers("Content-Type: application/xml")
@Body("<login \"user_name\"=\"{user_name}\" \"password\"=\"{password}\"/>")
void xml(@Param("user_name") String user, @Param("password") String password);

@RequestLine("POST /")
@Headers("Content-Type: application/json")
// json curly braces must be escaped!
@Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
void json(@Param("user_name") String user, @Param("password") String password);
}

public class Example {
public static void main(String[] args) {
client.xml("denominator", "secret"); // <login "user_name"="denominator" "password"="secret"/>
client.json("denominator", "secret"); // {"user_name": "denominator", "password": "secret"}
}
}

Headers

Feign支持API和单独客户端2个层面的headers配置。

API层面配置

如果接口内的多个方法都需要包含相同headers配置,可以将headers配置在API接口上。

同时在API接口和接口方法上使用@Headers

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

@Headers根据根据表达式的值,动态的从@Param内获取对应的内容。

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

有时可能需要动态的指定headers内的key和value。这类场景可以使用HeaderMap 来动态传入headers的key和value。

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

动态header

如果一个API接口会被不同的目标端使用,同时需要指定不同的headers,或者在发送请求时作特殊处理。可以通过RequestInterceptor 或者 Target来实现

使用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));
}
}

上述这些实现是在Feign client构建和使用时,通过自定义RequestInterceptorTarget 来设置headers,这种方式可以配置在每一个Feign client上,同时也会对所有的API接口生效。这是一个很实用的功能,比如为所有client的每一个请求header增加auth token。这些扩展方法与API方法调用使用同一个线程,所以在API方法运行时可以根据上下文动态的指定headers的内容,比如,可以使用ThreadLocal为每个请求线程存储不同的header内容,对于实现特殊的请求线程链路追踪是比较有用的方式。

高级用法

基础api

有时,某些服务的api会使用相同的约束。Feign可以使用接口单继承的方式进行配置。

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

@RequestLine("GET /all")
List<Entity> all();
}

通过继承来扩展基础api的能力。

interface CustomAPI extends BaseAPI {
@RequestLine("GET /custom")
String custom();
}

也可以通过泛型,约束API接口的操作对象类型。

@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

通过logger可以对API接口的调用日志作一些特殊处理。

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

拦截器

你可以使用RequestInterceptor拦截所有的请求,比如作为中间请求代理你可以设置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");
}
}

或是使用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);
}

动态查询参数

可以对Map类型参数使用QueryMap注解,Map类型参数的K-V就是查询的参数名及参数值

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

QueryMapEncoder同样可以通过JAVA BEAN生成查询参数。

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

当使用上面的方式,如果没有自定义QueryMapEncoder,那么查询参数名将默认使用对象的成员变量名生成。使用JAVA BEAN生成如下参数"/find?name={name}&number={number}"(如果成员变量的值为null,则参数会被忽略)

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

当使用@QueryMap注解对象时,默认编码器通过反射方式查找对象属性,并将属性转换成string类型。如果你希望使用getter和setter来构建查询字符串,你需要在Java Bean内定义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");
}
}

错误处理

通过Feign.builder为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 code为非2xx的响应都会触发 ErrorDecoderdecode方法,可以在该方法内对响应进行处理,包装自定义的异常或执行一些额外逻辑。

如果想要对失败请求进行重试,可以抛出一个RetryableException,则会使用已注册的Retryer进行重试.

重试

Feign默认会自动重试所有抛出了IOException的方法,也会自动重试ErrorDecoder抛出的RetryableException请求。可以通过builder注册自定义的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来决定是否进行重试,Retryer在每个Client创建时指定,可以根据不同的场景使用特定的重试策略。 如果重试失败最终的抛出一个RetryException异常。可以通过为Feign client配置exceptionPropagationPolicy()选项,会输出请求失败的原始异常信息。

静态方式和默认方法

Feign的目标接口可以包含静态方法或者默认方法(jdk版本需要1.8及以上)。可以用来定义不包含其它逻辑的Feign client基础api。比如,使用静态方法创建一个普通client;默认方法则可以被用来组装查询或定义默认参数。

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

文章目录
  1. 1. Why Feign and not X?
  2. 2. Feign如何工作?
  3. 3. Java 版本兼容
  4. 4. 基本使用方式
  5. 5. 接口注解
  6. 6. 表达式(Templates)与模板(Expressions)
    1. 6.1. 请求参数说明
    2. 6.2. 未定义的值和空值
      1. 6.2.1. 自定义扩展
    3. 6.3. 请求头扩展
    4. 6.4. 请求体扩展
  7. 7. 定制化服务
  8. 8. 多态
  9. 9. 更多示例
  10. 10. 集成能力
  11. 11. Gson
  12. 12. Jackson
  13. 13. Sax
  14. 14. JAXB
  15. 15. JAX-RS
  16. 16. OkHttp
  17. 17. Ribbon
  18. 18. Java 11 Http2
  19. 19. Hystrix
  20. 20. SOAP
  21. 21. SLF4J
  22. 22. Decoders
  23. 23. Encoders
  24. 24. @Body 模板
  25. 25. Headers
    1. 25.1. API层面配置
    2. 25.2. 动态header
  26. 26. 高级用法
    1. 26.1. 基础api
    2. 26.2. Logging
    3. 26.3. 拦截器
    4. 26.4. 自定义 @Param
    5. 26.5. 动态查询参数
  27. 27. 错误处理
  28. 28. 重试
    1. 28.1. 静态方式和默认方法