转载请注明出处: 翻译: Spring Cloud Feign使用文档Why Feign and not X?Feign使用诸如Jersey和CXF之类的工具来实现ReST或SOAP服务的java客户端, 此外, Feign允许你在http库(如: Apache HC)之上编写自己的代码. 通过自定义解码器(decoders)和错误处理(error handing), Feign可以用最小的开销和最少的代码将你的代码关联到任何基于文本的http接口(http APIS),How does Feign work?Feign是通过将注解(annotations)转换成模板请求来实现它的功能的, Feign可以将请求参数直接应用到这些模板上. 尽管Feign只支持基于文本的接口, 但同样的它能显著地简化系统的方方面面, 如请求重放等, 此外, Feign也可以使你的单元测试更加简单.Java Version CampatibilityFeign 10.x及以上的版本是基于Java 8构建的, 且应该同样支持Java 9、10、11, 如果你需要在JDK 6的版本上使用的话, 请使用Feign 9.x版本.Basics下面的代码是适配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 + “)”); } }}Interface AnnotationsFeign的注解定义了接口与底层http客户端功能之间的约定, 默认情况下各个注解的约定含义如下:AnnotationInterface TargetUsage@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的注解值解析对应的表达式Templates and ExpressionsFeign支持由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]}.Request Parameter ExpansionRequestLine和QueryMap遵循 URI Template - RFC 6570规范对一级模板(Level 1 templates)的规定:未被解析的值将会被忽略.所有未编码或者通过@Param注解标记为已编码(encoded)的字符和变量值都使用pct编码(pct-encoded).可以从Advanced Usage一节查看更多示例.What about slashes? /默认情况下, @RequestLine和@QueryMap模板不会对正斜杠/进行编码, 如果需要默认对其进行编码的话, 可以将@RequestLine的decodeSlash属性值设置为false.What about plus? +根据URI规范, +可以使用在URI地址和请求参数(query segments)这两个部分上, 然而在请求参数(query)上对该符号的处理却有可能不一致, 在一些遗留的系统上, +会被解析成一个空白符(space). 对此, Feign采用现代系统对+的解释, 不会将+认为是一个空白符(space), 并将请求参数上的+编码为%2B.如果你希望将+当成空白符(space), 那么请直接使用一个空格 或者直接将其编码为%20.Custom Expansion@Param注解有一个可选的参数expander可以用来控制单个参数的展开行为(expansion), 该属性的值必须指向一个实现了Expander接口的类:public interface Expander { String expand(Object value);}对该方法的返回值的处理与上述规则相同, 如果返回值是null或者是一个空字符串, 那么该值会被忽略. 如果返回值不是使用pct编码(pct-encoded)的, 将会自动转换成pct编码. 可以从 Custom @Param Expansion 一节查看更多示例.Request Headers Expansion@Headers和HeaderMap模板对 Request Parameter Expansion 一节阐述的规则做以下修改, 并遵循之:未被解析的值将会被忽略, 但如果解析到一个空的值(empty header value), 那么对应的请求头会被移除.不会对请求头使用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);}当你在设计你的接口的一定要牢记这一点.Reuqest Body ExpansionBody模板对 Request Parameter Expansion 一节阐述的规则做以下修改, 并遵循之:未被解析的值将会被忽略.展开的值在被解析到请求体之前不会经过Encoder处理.必须指定Content-Type请求头, 可以从 Body Templates一节查看示例.Customization你可以在很多地方对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”); }}Multiple InterfacesFeign客户以对使用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 / }}ExamplesFeign包含了GitHub和Wikipedia的客户端示例代码, 在实践中也可以参考这些项目, 尤其是example daemon.IntegrationsFeign在设计上就希望能够和其他开源项目很好的整合到一起, 我们也很乐于将你喜欢的模块添加进来.GsonGson包含了和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”); }}JacksonJackson包含了和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”); }}SaxSaxDecoder提供了可以与普通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”); }}JAXBJAXB包含了和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”); }}JAX-RSJAXRSContract使用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”); }}OkHttpOkHttpClient直接将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”); }}RibbonRibbonClient会覆盖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”); }}Java 11 Http2Http2Client直接将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”);HystrixHystrixFeign实现了由Hystrix提供的断路器功能.要将Hystrix与Feign整合, 你需要将Hystrix模块放到classpath下, 并使用HystrixFeign:public class Example { public static void main(String[] args) { MyService api = HystrixFeign.builder().target(MyService.class, “https://myAppProd”); }}SOAPSOAP包含了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.SLF4JSLF4JModule实现了将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”); }}DecodersFeign.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”); }}Encoders将一个请求体发送到服务器的最简单的办法是定义一个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 templates使用@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”} }}HeadersFeign支持在api上为每个请求设置请求头, 也支持为每个客户端的请求设置请求头, 你可以根据实际场景进行选择.Set headers using apis对于那些明确需要设置某些请求头的接口的情况, 适用于将请求头的定义作为接口的一部分.静态配置的请求头可以通过在接口上使用@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客户端做任何的定制.Setting headers per target当同一个接口的请求需要针对不同的请求对象(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里读取不同的数据设置请求头.Advanced usageBase Apis大多数情况下服务的接口都遵循相同的约定. 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> { }Logging你可以通过为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”); }}Request Interceptors如果你需要跨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”); }}Custom @Param Expansion使用@Param注解的参数会用其toString()方法展开获得参数值, 也可以通过制定一个自定义的Param.Expander来控制. 如对日期的格式化:public interface Api { @RequestLine(“GET /?since={date}”) Result list(@Param(value = “date”, expander = DateToMillis.class) Date date);}Dynamic Query Parameters可以通过对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”); }}Error Handling你可以通过在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.Retry默认情况下, 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()选项.Static and Default Methods使用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”); }}
...