转载请注明出处: 翻译: Spring Cloud Feign使用文档
Feign
使用诸如 Jersey
和 CXF
之类的工具来实现 ReST
或 SOAP
服务的java客户端, 此外, Feign
允许你在http库(如: Apache HC
)之上编写自己的代码. 通过自定义解码器( decoders
)和错误处理( error handing
), Feign
可以用最小的开销和最少的代码将你的代码关联到任何基于文本的http接口( http APIS
),
Feign
是通过将注解( annotations
)转换成模板请求来实现它的功能的, Feign
可以将请求参数直接应用到这些模板上. 尽管 Feign
只支持基于文本的接口, 但同样的它能显著地简化系统的方方面面, 如请求重放等, 此外, Feign
也可以使你的单元测试更加简单.
Feign 10.x
及以上的版本是基于Java 8构建的, 且应该同样支持Java 9、10、11, 如果你需要在JDK 6的版本上使用的话, 请使用 Feign 9.x
版本.
下面的代码是适配
Retrofit示例
的用法:
interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); } public static 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"); // 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
的注解定义了接口与底层http客户端功能之间的约定, 默认情况下各个注解的约定含义如下:
Annotation | Interface Target | Usage |
---|---|---|
@RequestLine
|
接口 |
定义请求的 HttpMethod
和 UriTemplate
. 模板中可以使用大括号包围的表达式( {expression}
), 表达式的值由 @Param
对应参数的注解值提供. |
@Param
|
参数 | 定义模板变量, 变量的值应该由名字相对应的表达式提供. |
@Headers
|
方法、 Type
|
定义 HeaderTemplate
; 使用 @Param
注解的值解析对应的表达式. 当该注解应用在 Type
上时, 该模板会被应用到每一个请求上. 当该注解应用在方法上时, 该模板仅会被应用到对应的方法上. |
@QueryMap
|
参数 |
将键值对类型的Map、POJO展开成地址上的请求参数( query string
) |
@HeaderMap
|
参数 |
将键值对类型的Map展开成请求头 Http Headers
. |
@Body
|
方法 |
定义与 UriTemplate
和 HeaderTemplate
类似的模板( Template
), 该模板可以使用 @Param
的注解值解析对应的表达式 |
Feign
支持由 URI Template - RFC 6570
定义的简单字符串(Level 1)表达式, 表达式的值从相关方法上对应 @Param
注解提供, 示例如下:
public interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> getContributors(@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"); } }
表达式必须使用大括号( {}
)包裹着, 并且支持使用冒号( :
)分隔的正则表达式来限定表达式的值. 如限定上述例子的 owner
参数的值必须是字母: {owner:[a-zA-Z]*}
.
RequestLine
和 QueryMap
遵循 URI Template - RFC 6570
规范对一级模板( Level 1 templates
)的规定:
@Param
注解标记为已编码( encoded
)的字符和变量值都使用 pct编码(pct-encoded)
. 可以从 Advanced Usage 一节查看更多示例.
/
默认情况下, @RequestLine
和 @QueryMap
模板不会对正斜杠 /
进行编码, 如果需要默认对其进行编码的话, 可以将 @RequestLine
的 decodeSlash
属性值设置为 false
.
What about plus? +
根据URI规范, +
可以使用在 URI
地址和请求参数( query segments
)这两个部分上, 然而在请求参数(query)上对该符号的处理却有可能不一致, 在一些遗留的系统上, +
会被解析成一个空白符( space
). 对此, Feign
采用现代系统对 +
的解释, 不会将 +
认为是一个空白符( space
), 并将请求参数上的 +
编码为 %2B
.
如果你希望将 +
当成空白符( space
), 那么请直接使用一个空格
或者直接将其编码为
%20
.
@Param
注解有一个可选的参数 expander
可以用来控制单个参数的展开行为( expansion
), 该属性的值必须指向一个实现了 Expander
接口的类:
public interface Expander { String expand(Object value); }
对该方法的返回值的处理与上述规则相同, 如果返回值是 null
或者是一个空字符串, 那么该值会被忽略. 如果返回值不是使用 pct
编码( pct-encoded
)的, 将会自动转换成 pct
编码. 可以从 Custom @Param Expansion
一节查看更多示例.
@Headers
和 HeaderMap
模板对 Request Parameter Expansion
一节阐述的规则做以下修改, 并遵循之:
pct
编码( pct-encoding
). 可以从 Headers 一节查看示例.
关于 @Param
参数和参数名需要注意的点
无论是在 @RequestLine
、 @QueryMap
、 @BodyTemplate
还是 @Headers
上的表达式, 只要表达式内的变量名字相同, 那么它们的值也必然相同. 如下面的例子, contentType
的值会同时被解析到请求头(header)和路径(path)上:
public interface ContentService { @RequestLine("GET /api/documents/{contentType}") @Headers("Accept: {contentType}") String getDocumentByType(@Param("contentType") String type); }
当你在设计你的接口的一定要牢记这一点.
Body
模板对 Request Parameter Expansion
一节阐述的规则做以下修改, 并遵循之:
Encoder
处理. Content-Type
请求头, 可以从 Body Templates
一节查看示例.
你可以在很多地方对 Feign
进行定制. 比如, 你可以使用 Feign.builder()
对自定义的组件构建API接口:
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
客户以对使用 Target<T>
(默认是 HardCodedTarget<T>
)定义的对象生成多个API接口, 这样你可以在执行前动态发现服务或者对请求进行装饰.
例如, 下面的代码 可以实现为
从身份服务中获取当前 url
和 授权令牌(auth token)
, 然后设置到每个请求上:
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
包含了 GitHub
和 Wikipedia
的客户端示例代码, 在实践中也可以参考这些项目, 尤其是 example daemon
.
Feign
在设计上就希望能够和其他开源项目很好的整合到一起, 我们也很乐于将你喜欢的模块添加进来.
Gson
包含了和JSON接口相关的编码( GsonEncoder
)、解码器( GsonDecoder
), 将它将它用到 Feign.Builder
的方式如下:
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
包含了和JSON接口相关的编码( JacksonEncoder
)、解码器( JacksonDecoder
), 将它将它用到 Feign.Builder
的方式如下:
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"); } }
SaxDecoder 提供了可以与普通JVM和Android环境兼容的方式解析XML文本, 下面的例子展示了如何使用:
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
包含了和XML接口相关的编码器( JAXBEncoder
)、解码器( JAXBEncoder
), 将它将它用到 Feign.Builder
的方式如下:
public class Example { public static void main(String[] args) { Api api = Feign.builder() .encoder(new JAXBEncoder()) .decoder(new JAXBDecoder()) .target(Api.class, "https://apihost"); } }
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"); } }
OkHttpClient
直接将 Feign
的http请求直接交由 OkHttp
处理, 后者实现了SPDY协议和提供了更好的网络控制能力.
将 OkHttp
整合到 Feign
中需要你把 OkHttp
模块放到 classpath
下, 然后做如下配置:
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .client(new OkHttpClient()) .target(GitHub.class, "https://api.github.com"); } }
RibbonClient
会覆盖 Feign
客户端的URL解析, 以实现由 Ribbon
提供的智能路由和弹性能力.
将 Ribbon
与 Feign
整合需要你将url中的 主机名(host)
部分替换成 Ribbon
客户端名. 例如 Ribbon
客户端明为 myAppProd
:
public class Example { public static void main(String[] args) { MyService api = Feign.builder() .client(RibbonClient.create()) .target(MyService.class, "https://myAppProd"); } }
Http2Client
直接将 Feign
的http请求交给Java11 New HTTP/2 Client
处理, 后者实现了HTTP/2协议.
要将 New HTTP/2 Client
与 Feign
整合使用, 你需要使用Java SDK 11, 并做如下配置:
GitHub github = Feign.builder() .client(new Http2Client()) .target(GitHub.class, "https://api.github.com");
HystrixFeign 实现了由 Hystrix 提供的断路器功能.
要将 Hystrix
与 Feign
整合, 你需要将 Hystrix
模块放到 classpath
下, 并使用 HystrixFeign
:
public class Example { public static void main(String[] args) { MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd"); } }
SOAP
包含了XML接口相关的编码器( SOAPEncoder
)、解码器( SOAPDecoder
).
该模块通过JAXB和SOAPMessage实现了对 SOAP Body
的编码和解码的支持, 通过将 SOAPFault
包装秤 javax.xml.ws.soap.SOAPFaultException
实现了对 SOAPFault
解码的功能, 因此, 对于 SOAPFault
的处理, 你只需要捕获 SOAPFaultException
.
使用示例如下:
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 Faults
的响应使用了表示错误的状态码(4xx, 5xx, …)的话, 那么你还需要添加一个 SOAPErrorDecoder
.
SLF4JModule
实现了将 Feign
的日志重定向到 SLF4J
, 这允许你很容易的就能使用你想用的日志后端(Logback、Log4J等).
要将 SLF4J
与 Feign
整合, 你需要将 SLF4J
模块和对应的日志后端模块放到 classpath
下, 并做如下配置:
public class Example { public static void main(String[] args) { GitHub github = Feign.builder() .logger(new Slf4jLogger()) .target(GitHub.class, "https://api.github.com"); } }
Feign.builder()
允许你手动指定额外的配置, 如配置如何对响应进行解析.
如果你接口定义的方法的返回值是除了 Response
、 String
、 byte[]
或 void
之外的类型, 那么你必须配置一个非默认的 Decoder
.
下面的代码展示了如何配置使用 feign-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
方法, 下面的代码展示了对一个jsonp响应的处理, 在将响应交给JSON解码器之前, 需要先对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"); } }
将一个请求体发送到服务器的最简单的办法是定义一个 POST
请求方法, 该方法的参数类型是 String
或 byte[]
, 且参数上不带任何注解, 并且你可能还需要设置 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
, 你可以发送一个类型安全的请求体, 下面的例子展示了使用 feign-gson
扩展来实现编码:
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
注解的模板会使用 @Param
注解的值来展开模板内部的表达式, 对于 POST
请求你可能还需要设置 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"} } }
Feign
支持在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
使用变量展开动态指定请求头的内容:
public interface Api { @RequestLine("POST /") @Headers("X-Ping: {token}") void post(@Param("token") String token); }
有时候, 对于同一个接口或客户端的请求头, 其键和值可能会随着不同的方法调用而发生变化, 且不可预知(例如: 自定义元数据请求头字段"x-amz-meta- "或"x-goog-meta-
"), 此时可以在接口上声明一个Map参数, 并使用 @HeaderMap
注解将Map的内容设置为对应请求的请求头:
public interface Api { @RequestLine("POST /") void post(@HeaderMap Map<String, Object> headerMap); }
上述的几个方法都可以在接口上指定请求的请求头, 且不需要在构造时对 Feign
客户端做任何的定制.
当同一个接口的请求需要针对不同的请求对象( endpoints
)配置不同的请求头, 或者需要对同一个接口的每个请求都定制其请求头时, 可以在 Feign
客户端上使用 RequestInterceptor
或 Target
来设置请求头.
使用 RequestInterceptor
设置请求头的例子可以在 Request Interceptor
一节中查看示例.
使用 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)); } }
上述方法的最终效果取决于你对 RequestInterceptor
或 Target
内部的实现, 可以通过这种方法对每个 Feign
客户端的所有接口调用设置请求头. 这在一些场景下是非常有用的, 如对每个 Feign
客户端的所有请求设置认证令牌 authentication token
. 这些方法是在接口调用者所在的线程中执行的(译者注: 需要注意线程安全), 因此请求头的值可以是在调用时根据上下文动态地设置. 例如, 可以根据不同的调用线程, 从 ThreadLocal
里读取不同的数据设置请求头.
大多数情况下服务的接口都遵循相同的约定. Feign
使用单继承的方式来实现, 比如下面的例子:
interface BaseAPI { @RequestLine("GET /health") String health(); @RequestLine("GET /all") List<Entity> all(); }
你可以通过继承的方式来拥有 BaseAPI
的接口, 并实现其他特定的接口:
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> { }
你可以通过为 Feign
客户端设置 Logger
来记录其http日志, 最简单的实现如下:
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"); } }
如果你需要跨 Feign
客户端对所有请求都做修改, 那么你可以配置 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
注解的参数会用其 toString()
方法展开获得参数值, 也可以通过制定一个自定义的 Param.Expander
来控制. 如对日期的格式化:
public interface Api { @RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date); }
可以通过对Map类型的参数加上 QueryMap
注解, 将Map的内容构造成查询参数( query parameters
):
public interface Api { @RequestLine("GET /find") V find(@QueryMap Map<String, Object> queryMap); }
同样的, 也可以通过使用 QueryMapEncoder
实现用POJO对象生成查询参数( query parameter
):
public interface Api { @RequestLine("GET /find") V find(@QueryMap CustomPojo customPojo); }
当用这种方式时, 如果没有指定一个自定义的 QueryMapEncoder
, 那么查询参数的( query parameter
)内容将根据对象的成员变量生成, 参数名对应变量名. 下面的例子中, 根据POJO对象生成的查询参数( query parameter
)的内容是"/find?name={name}&number={number}", 生成的查询参数的顺序是不固定的, 按照惯例, 如果POJO对象的某个变量值为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"); } }
当用 @QueryMao
注解时, 默认的编码器( encoder
)会对对象的字段使用反射来将其展开成查询参数( query string
). 如果希望通过对象的getter和setter方法来展开查询参数( query string
), 请使用 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
实例构造时注册一个自定义的 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
.
默认情况下, Feign
会对产生 IOException
的请求自动重试, 无论使用的是哪种HTTP方法, 都认为 IOExcdeption
是由短暂的网络问题产生的. 对 ErrorDecoder
内抛出的 RetryableException
也会进行请求重试. 你也可以通在 Feign
实例构造时设置自定义的 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
)来实现. 每个 Feign
客户端执行时都会构造一个 Retryer
实例, 这样的话你可以维护每个请求的重新状态.
如果最终重试也失败了, 那么会抛出 RetryException
, 如果希望抛出导致重试失败的异常, 可以在构造 Feign
客户端时指定 exceptionPropagationPolicy()
选项.
使用 Feign
的接口可能是静态的或默认的方法(Java 8及以上支持), 这允许 Feign
客户端包含一些不适用底层接口定义的逻辑. 例如, 使用静态方法可以很轻易地指定通用客户端构造配置, 使用默认方法可以用于组合查询或定义默认参数:
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"); } }