Feign 讓編寫java http客戶端變簡(jiǎn)單
Fegin是一個(gè)java調(diào)用HTTP的客戶端binder,其靈感來(lái)自于Retrofit、JAXRS-2.0和WebSocket。Feign的首要目標(biāo)是降低將Denominator統(tǒng)一的綁定到HTTP的復(fù)雜性,而無(wú)需關(guān)心是否為RESTfull.
Why Feign and not X?
Feign的使用方式與Jersey和CXF編寫java client來(lái)調(diào)用REST或SOAP服務(wù)類似。此外,F(xiàn)eign還允許編寫基于Apache HC之類的類庫(kù)的代碼。Feign通過(guò)自定義decoder和error處理以最小的成本將你的代碼連接到http,并可以使用到任何基于文本的http API上。
Feign如何工作?
Feign基于將注解轉(zhuǎn)換成請(qǐng)求模板的方式工作。參數(shù)會(huì)簡(jiǎn)單直接的應(yīng)用到模板上。盡管Feign只支持基于文本的API,但是它的簡(jiǎn)化了類似于請(qǐng)求重放等系統(tǒng)層面的開發(fā)。此外,F(xiàn)eign讓單元測(cè)試更加方便。
Java 版本兼容
Feign 10.x版本基于java 1.8同時(shí)可以在java9,10和11版本上工作。如果要在JDK6上使用,請(qǐng)使用Feign 9.x
基本使用方式
下面是一個(gè)典型的用法,適配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,F(xiàn)eign默認(rèn)Contract 如下表。
| Annotation | Interface Target | Usage |
|---|---|---|
@RequestLine |
Method | 用來(lái)定義request 的 HttpMethod和UriTemplate。表達(dá)式(expression)使用大括號(hào){}括起來(lái),表達(dá)式的值會(huì)由被@Param注解的參數(shù)提供。 |
@Param |
Parameter | 定義模板變量,變量的值會(huì)根據(jù)參數(shù)名解析到Expression內(nèi)。 |
@Headers |
Method或Interface | 定義HeaderTemplate,表達(dá)式的值會(huì)由被@Param注解的參數(shù)提供。@Header可以使用到Method或Interface上,當(dāng)作用到Interface上時(shí)Interface內(nèi)的所有方法都會(huì)使用該Header,如果作用到Method上那只會(huì)對(duì)該方法使用Header。 |
@QueryMap |
Parameter | 可以修飾K-V結(jié)構(gòu)的Map或POJO,用以擴(kuò)展查詢參數(shù)。 |
@HeaderMap |
Parameter | 可以修飾K-V結(jié)構(gòu)的Map或POJO,用以擴(kuò)展Http Header。 |
@Body |
Method | 與UriTemplate和HeaderTemplate類似,用來(lái)定義一個(gè)Template,表達(dá)式的值會(huì)由被@Param注解的參數(shù)提供。 |
表達(dá)式(Templates)與模板(Expressions)
Feign的Expressions遵循URI Template - RFC 6570 level 1規(guī)范。Expressions需要配合Param所修飾的方法參數(shù)進(jìn)行擴(kuò)展。
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");
}
}
- 表達(dá)式(Expression):被花括號(hào)括起來(lái)的內(nèi)容,可以使用正則表達(dá)式,表達(dá)式名稱與正則表達(dá)式用冒號(hào)
:分隔,比如{owner:[a-zA-Z]*}。表達(dá)式的值由被@Param修飾的變量提供。 - 模板(Template):多個(gè)表達(dá)式的集合,如
GET /repos/{owner}/{repo}/contributors
請(qǐng)求參數(shù)說(shuō)明
RequestLine 和 QueryMap 的模板遵循URI Template - RFC 6570 level 1規(guī)范,具體的規(guī)范如下:
- 忽略掉無(wú)法解析的表達(dá)式
- 如果沒(méi)有對(duì)字符串或變量進(jìn)行編碼或未通過(guò)
@Param注解期編碼方式,默認(rèn)會(huì)使用pct-encoded編碼。
未定義的值和空值
未定義的表達(dá)式也是合法的表達(dá)式,如果明確指定表達(dá)式的值為Null或者為空,遵循URI Template - RFC 6570規(guī)范,則會(huì)提供一個(gè)空的值給表達(dá)式。
當(dāng)Feign解析表達(dá)式時(shí),會(huì)優(yōu)先判斷是否定義了表達(dá)式的值來(lái)決定是否要保留查詢參數(shù)名。如果未定義表達(dá)式的值,查詢參數(shù)名將會(huì)被去除,看下面的示例。
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
高級(jí)用法 提供了更多的示例.
關(guān)于斜杠
/默認(rèn)情況下
@RequestLine和@QueryMap模板不會(huì)對(duì)/編碼,如果要對(duì)其編碼可以設(shè)置@RequestLine的decodeSlash值為false。
關(guān)于加號(hào)
+根據(jù)URI規(guī)范,請(qǐng)求地址和請(qǐng)求參數(shù)是允許使用加號(hào)
+的,但是在查詢的處理上可能會(huì)不一致。在一些舊的系統(tǒng)上+可能會(huì)被當(dāng)作空白字符。Feign根據(jù)現(xiàn)代系統(tǒng)的做法不會(huì)將+當(dāng)作空白字符,而是編碼為%2B。如果要將
+處理成空白字符,可以使用空格或者直接將其編碼為%20
自定義擴(kuò)展
@Param可以通過(guò)可選屬性expander對(duì)某一參數(shù)進(jìn)行擴(kuò)展處理。expander屬性需要接收一個(gè)實(shí)現(xiàn)了Expander接口的實(shí)現(xiàn)類。
public interface Expander {
String expand(Object value);
}
方法的返回值規(guī)則與上述規(guī)則相同。忽略掉無(wú)法解析的表達(dá)式;
如果沒(méi)有對(duì)字符串或變量進(jìn)行編碼或未通過(guò)@Param注解期編碼方式,默認(rèn)會(huì)使用pct-encoded編碼。更多使用示例參考Custom @Param Expansion
請(qǐng)求頭擴(kuò)展
Headers 與 HeaderMap 模板規(guī)則與Request Parameter Expansion 規(guī)則相同
- 如果返回值為null或空,則移除整個(gè)header
- 如果未進(jìn)行pct編碼,則會(huì)進(jìn)行pct編碼
Headers 示例代碼.
請(qǐng)注意
@Param參數(shù)和參數(shù)名稱:如果
@RequestLine,@QueryMap,@BodyTemplate, 或@Headers擁有相同的表達(dá)式名稱,那么這些表達(dá)式會(huì)被賦予相同的值。如下示例中的contentType將會(huì)被同時(shí)解析到@RequestLine和@Headers的表達(dá)式中
public interface ContentService { @RequestLine("GET /api/documents/{contentT?ype}") @Headers("Accept: {contentType}") String getDocumentByType(@Param("contentType") String type); }在設(shè)計(jì)接口時(shí)需要額外注意。
請(qǐng)求體擴(kuò)展
Body模板的規(guī)則與Request Parameter Expansion 相同:
- 忽略掉無(wú)法解析的表達(dá)式
- 擴(kuò)展內(nèi)容在被傳遞到請(qǐng)求體前不會(huì)被編碼
- 必須在header內(nèi)指定
Content-Type。參考示例代碼 Body Templates。
定制化服務(wù)
Feign同時(shí)也提供了一些定制化的能力,你可以使用Feign.builder()來(lái)構(gòu)建自定義的接口。
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");
}
}
多態(tài)
Feign支持創(chuàng)建多個(gè)api接口。通過(guò)繼承接口Target<T>(默認(rèn)實(shí)現(xiàn)為HardCodedTarget<T>)實(shí)現(xiàn),在程序運(yùn)行request前會(huì)動(dòng)態(tài)的發(fā)現(xiàn)并包裝請(qǐng)求。
例如,下面的場(chǎng)景對(duì)當(dāng)前使用的url增加CloudIdentityTarget進(jìn)行auth驗(yàn)證。
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的客戶端示例代碼,以供大家作為開發(fā)參考。特殊的示例請(qǐng)參考example daemon.
集成能力
Feign設(shè)計(jì)原則就是能夠與其它開源工具友好的工作,也歡迎將您喜歡的項(xiàng)目集成進(jìn)來(lái)。
Gson
Gson 包含了可以與JSON API共同使用的編碼器和解碼器
使用Feign.Builder添加GsonEncoder 或 GsonDecoder
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添加JacksonEncoder 或 JacksonDecoder
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 可以在標(biāo)準(zhǔn)的JVM或Android環(huán)境下解碼XML。
使用Sax對(duì)響應(yīng)數(shù)據(jù)進(jìn)行解析
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添加JAXBEncoder 或 JAXBDecoder
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對(duì)標(biāo)準(zhǔn)的JAX-RS進(jìn)行了重新處理,目前基于1.1規(guī)范。
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請(qǐng)求交由OkHttp處理,OkHttp使用了SPDY協(xié)議,可以更好的對(duì)網(wǎng)絡(luò)進(jìn)行控制
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解析,同時(shí)提供了智能路由和彈性能力。
集成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請(qǐng)求交由Java11 New HTTP/2 Client處理以實(shí)現(xiàn)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共同使用的編碼器和解碼器
該模塊通過(guò)JAXB和SOAPMessage對(duì)SOAP Body提供編碼和解碼。同時(shí)將SOAPFault解碼能力包裝到了原生的javax.xml.ws.soap.SOAPFaultException,所以使用時(shí)只需要捕獲SOAPFaultException來(lái)處理SOAPFault。
Add SOAPEncoder and/or SOAPDecoder to your Feign.Builder like so:
使用Feign.Builder來(lái)添加SOAPEncoder 或 SOAPDecoder。
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的錯(cuò)誤響應(yīng)包含http code (4xx、5xx、…),則需要添加SOAPErrorDecoder對(duì)錯(cuò)誤進(jìn)行處理。
SLF4J
SLF4JModule直接將Feign's logging交由SLF4J處理,你可以方便的選擇logging的后端實(shí)現(xiàn)。
若要使用SLF4J,需要添加SLF4J和對(duì)應(yīng)的后端模塊到classpath內(nèi),然后使用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對(duì)響應(yīng)數(shù)據(jù)進(jìn)行解碼。
如果接口內(nèi)某些方法反回了除Response, String, byte[] 或 void之外的返回值,則需要配置一個(gè)自定義的 Decoder。
使用gson對(duì)json進(jìn)行解碼
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
}
}
如果你需要在解碼器解碼之前作一些前置操作,可以使用mapAndDecode。
舉個(gè)例子,現(xiàn)在要求API只處理jsonp,可以在傳輸之前不對(duì)jsonp進(jìn)行包裝。
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.
一個(gè)最簡(jiǎn)單的請(qǐng)求方式就是使用POST方法傳遞一個(gè)String或byte[]類型參數(shù)到服務(wù)端,有時(shí)可能需要在header內(nèi)指定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配置方式,這樣就可以發(fā)送一些類型安全的請(qǐng)求體。
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注解用來(lái)聲明一個(gè)請(qǐng)求體模板,并配合@Param注解對(duì)請(qǐng)求參數(shù)進(jìn)行擴(kuò)展。在使用時(shí)需要指定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和單獨(dú)客戶端2個(gè)層面的headers配置。
API層面配置
如果接口內(nèi)的多個(gè)方法都需要包含相同headers配置,可以將headers配置在API接口上。
同時(shí)在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根據(jù)根據(jù)表達(dá)式的值,動(dòng)態(tài)的從@Param內(nèi)獲取對(duì)應(yīng)的內(nèi)容。
public interface Api {
@RequestLine("POST /")
@Headers("X-Ping: {token}")
void post(@Param("token") String token);
}
有時(shí)可能需要?jiǎng)討B(tài)的指定headers內(nèi)的key和value。這類場(chǎng)景可以使用HeaderMap 來(lái)動(dòng)態(tài)傳入headers的key和value。
public interface Api {
@RequestLine("POST /")
void post(@HeaderMap Map<String, Object> headerMap);
}
動(dòng)態(tài)header
如果一個(gè)API接口會(huì)被不同的目標(biāo)端使用,同時(shí)需要指定不同的headers,或者在發(fā)送請(qǐng)求時(shí)作特殊處理??梢酝ㄟ^(guò)RequestInterceptor 或者
Target來(lái)實(shí)現(xiàn)
使用RequestInterceptor的例子可以參考Request Interceptors章節(jié)。下面是使用Target的實(shí)現(xiàn)方式。
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));
}
}
上述這些實(shí)現(xiàn)是在Feign client構(gòu)建和使用時(shí),通過(guò)自定義RequestInterceptor 或 Target 來(lái)設(shè)置headers,這種方式可以配置在每一個(gè)Feign client上,同時(shí)也會(huì)對(duì)所有的API接口生效。這是一個(gè)很實(shí)用的功能,比如為所有client的每一個(gè)請(qǐng)求header增加auth token。這些擴(kuò)展方法與API方法調(diào)用使用同一個(gè)線程,所以在API方法運(yùn)行時(shí)可以根據(jù)上下文動(dòng)態(tài)的指定headers的內(nèi)容,比如,可以使用ThreadLocal為每個(gè)請(qǐng)求線程存儲(chǔ)不同的header內(nèi)容,對(duì)于實(shí)現(xiàn)特殊的請(qǐng)求線程鏈路追蹤是比較有用的方式。
高級(jí)用法
基礎(chǔ)api
有時(shí),某些服務(wù)的api會(huì)使用相同的約束。Feign可以使用接口單繼承的方式進(jìn)行配置。
interface BaseAPI {
@RequestLine("GET /health")
String health();
@RequestLine("GET /all")
List<Entity> all();
}
通過(guò)繼承來(lái)擴(kuò)展基礎(chǔ)api的能力。
interface CustomAPI extends BaseAPI {
@RequestLine("GET /custom")
String custom();
}
也可以通過(guò)泛型,約束API接口的操作對(duì)象類型。
@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
通過(guò)logger可以對(duì)API接口的調(diào)用日志作一些特殊處理。
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攔截所有的請(qǐng)求,比如作為中間請(qǐng)求代理你可以設(shè)置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來(lái)實(shí)現(xiàn)一個(gè)身份驗(yàn)證攔截器。
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注解的參數(shù)最終都是通過(guò)toString返回參數(shù)值,通過(guò)Param.Expander設(shè)置自定義擴(kuò)展,比如格式化日期。
public interface Api {
@RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
}
動(dòng)態(tài)查詢參數(shù)
可以對(duì)Map類型參數(shù)使用QueryMap注解,Map類型參數(shù)的K-V就是查詢的參數(shù)名及參數(shù)值
public interface Api {
@RequestLine("GET /find")
V find(@QueryMap Map<String, Object> queryMap);
}
QueryMapEncoder同樣可以通過(guò)JAVA BEAN生成查詢參數(shù)。
public interface Api {
@RequestLine("GET /find")
V find(@QueryMap CustomPojo customPojo);
}
當(dāng)使用上面的方式,如果沒(méi)有自定義QueryMapEncoder,那么查詢參數(shù)名將默認(rèn)使用對(duì)象的成員變量名生成。使用JAVA BEAN生成如下參數(shù)"/find?name={name}&number={number}"(如果成員變量的值為null,則參數(shù)會(huì)被忽略)
public class CustomPojo {
private final String name;
private final int number;
public CustomPojo (String name, int number) {
this.name = name;
this.number = number;
}
}
設(shè)置自定義的QueryMapEncoder
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.queryMapEncoder(new MyCustomQueryMapEncoder())
.target(MyApi.class, "https://api.hostname.com");
}
}
當(dāng)使用@QueryMap注解對(duì)象時(shí),默認(rèn)編碼器通過(guò)反射方式查找對(duì)象屬性,并將屬性轉(zhuǎn)換成string類型。如果你希望使用getter和setter來(lái)構(gòu)建查詢字符串,你需要在Java Bean內(nèi)定義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");
}
}
錯(cuò)誤處理
通過(guò)Feign.builder為Feign實(shí)例注冊(cè)一個(gè)自定義的ErrorDecoder,可以對(duì)異常的響應(yīng)作額外的處理。
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的響應(yīng)都會(huì)觸發(fā) ErrorDecoder的decode方法,可以在該方法內(nèi)對(duì)響應(yīng)進(jìn)行處理,包裝自定義的異?;驁?zhí)行一些額外邏輯。
如果想要對(duì)失敗請(qǐng)求進(jìn)行重試,可以拋出一個(gè)RetryableException,則會(huì)使用已注冊(cè)的Retryer進(jìn)行重試.
重試
Feign默認(rèn)會(huì)自動(dòng)重試所有拋出了IOException的方法,也會(huì)自動(dòng)重試ErrorDecoder拋出的RetryableException請(qǐng)求。可以通過(guò)builder注冊(cè)自定義的Retryer來(lái)實(shí)現(xiàn)自定義的重試策略。
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.retryer(new MyRetryer())
.target(MyApi.class, "https://api.hostname.com");
}
}
Retryer根據(jù)方法continueOrPropagate(RetryableException e)返回true 或者false來(lái)決定是否進(jìn)行重試,Retryer在每個(gè)Client創(chuàng)建時(shí)指定,可以根據(jù)不同的場(chǎng)景使用特定的重試策略。
如果重試失敗最終的拋出一個(gè)RetryException異常??梢酝ㄟ^(guò)為Feign client配置exceptionPropagationPolicy()選項(xiàng),會(huì)輸出請(qǐng)求失敗的原始異常信息。
靜態(tài)方式和默認(rèn)方法
Feign的目標(biāo)接口可以包含靜態(tài)方法或者默認(rèn)方法(jdk版本需要1.8及以上)??梢杂脕?lái)定義不包含其它邏輯的Feign client基礎(chǔ)api。比如,使用靜態(tài)方法創(chuàng)建一個(gè)普通client;默認(rèn)方法則可以被用來(lái)組裝查詢或定義默認(rèn)參數(shù)。
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");
}
}