Ribbon
RibbonClient 重写了 Feign 对 URL 的解析逻辑,实现了智能路由、负载均衡和重试机制。
引入 feign-ribbon 包,用 Feign client 的名字代替 API 的域名/IP+Port :
public class Example {
public static void main(String[] args) {
MyService api = Feign.builder()
.client(RibbonClient.create())
.target(MyService.class, "https://myAppProd");
}
}
Hystrix
HystrixFeign 实现了断路器功能。
要使用断路器功能,要引入 feign-hystrix 包。
public class Example {
public static void main(String[] args) {
MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd");
}
}
SLF4J
SLF4JModule 支持将 Feign 的日志写入 SLF4J(Logback, Log4J 等)。
要使用该功能,要引入 feign-slf4j 包和 Logback/Log4J/ 包。
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.logger(new Slf4jLogger())
.logLevel(Level.FULL)
.target(GitHub.class, "https://api.github.com");
}
}
Decoders
如果 Interface 中的 method 返回的类型不是 Response、String、byte[]、void,那么就需要自定义 Decoder 了,因为默认的 Decoder 仅支持这几种类型的解码(默认 Decoder 在 feign.codec 包下)。
解析 JSON 的 Decoder 配置示例(使用 feign-gson 扩展包):
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
}
}
在某些场景中,Decoder 对响应内容做解码之前需要对响应内容先做一些处理,Feign.Builder 提供了 mapAndDecode 函数支持这类场景。比如要从一个响应内容是 HTML 网页或者 XML 的 API 中抽取出有用的信息,组装成 JSON 格式的字符串,然后再使用 GonDecoder 将 JSON 字符串解码成业务对象类型;再比如目标 API 的响应内容是 jsonp (一种 json 使用模式)格式的,GsonDecoder 是无法直接做解码的,这时就要对 API 的响应内容先做一次处理,然后再用 GsonDecoder 对处理后的内容做解码:
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
定义一个参数类型是 String 或者 byte[] 的 method,向 POST 接口发送 request body :
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\"}");
}
}
上面的代码不安全,也不易读,更好的做法是将 method 参数声明成自定义类型,并给 Feign client 指定 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
@Headers
使用注解添加请求头
固定的键值对放到 Interface 或者 method 上:
@Headers("Accept: application/json")
interface BaseApi<V> {
@Headers("Content-Type: application/json")
@RequestLine("PUT /api/{key}")
void put(@Param("key") String key, V value);
}
动态键值对可以使用变量表达式:
public interface Api {
@RequestLine("POST /")
@Headers("X-Ping: {token}")
void post(@Param("token") String token);
}
某些场景下请求头中的键值对都是动态的,这时可以将 method 参数声明成 Map 类型,使用 @HeaderMap 实现动态请求头拼装:
public interface Api {
@RequestLine("POST /")
void post(@HeaderMap Map<String, Object> headerMap);
}
给所有的 target 请求统一添加 Header
要给所有的 target 的 Request 统一添加请求头,需要自定义实现 RequestInterceptor(要保证线程安全),RequestInterceptor 会作用到 target 的所有 method 。
如果要给每一个 target 甚至是每一个的 method 定制独特的请求头,那就要自定义 Target 了,因为 RequestInterceptor 无法访问 method 的元数据。如果用前面介绍的 @Header @Param 的方式实现定制化就会有大量重复代码,甚至无法达到用户的目的。
使用 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));
}
}
示例中的 provider 和 requestIdProvider 都是 ThreadLocal 类型的,示例演示了如何给 Bank 这个 Interface (即 feign client)中的 method 添加认证请求头和请求标识(常用于实现调用链路跟踪)。
要给所有的 feign client 统一添加请求头,得自定义 RequestInterceptor;
只给某个 feign client 或者多个 feign client 中的 具有共性的 method 添加请求头,得自定义 Target ,在构建 feign client 时使用 Feign.builder().target(new DynamicAuthTokenTarget(Bank.class, provider, requestIdProvider));
给指定的 feign client 添加请求头处理逻辑。
解释:具有共性的 method 在代码中随处可见,对多个类中具有共性的 method 的统一处理的常用思路是切面编程 AOP 、拦截器等,在 feign 中,利用 Target 处理多个 feign client 中具有共性的 method 的方式也是这种思路。
高级用法
Basics API
feign 支持接口继承,公共部分可以放到父接口:
@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 提供的记录请求响应日志的简单实现:
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.logger(new Logger.JavaLogger("GitHub.Logger").appendToFile("logs/http.log"))
.logLevel(Logger.Level.FULL)
.target(GitHub.class, "https://api.github.com");
}
}
Request Interceptors
RequstInterceptor 常用场景之一
给请求头添加代理记录追踪 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");
}
}
RequstInterceptor 常用场景之二
给请求头添加认证身份,Feign 提供了 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);
}
动态查询参数
用 @QueryMap 注解 Map 对象 构建 Request 参数:
public interface Api {
@RequestLine("GET /find")
V find(@QueryMap Map<String, Object> queryMap);
}
用 @QueryMap 注解 POJO 对象 构建 Request 参数:
public interface Api {
@RequestLine("GET /find")
V find(@QueryMap CustomPojo customPojo);
}
如果没有指定 QueryMapEncoder ,那么请求参数的 name 就是 POJO 对象的属性名。如果 POJO 属性值是 null ,那么该属性会被忽略。下面的 POJO 作为 method 参数的话,那么生成 URL 就是这样的 “/find?name={name}&number={number}”:
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");
}
}
不自定义 QueryMapEncoder 的情况下,默认的 Encoder 会使用反射处理 POJO 的属性。如果不习惯用构造器定义 POJO,而习惯使用 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");
}
}
Error Handling
HTTP 状态码不是 2xx 的响应都会触发 ErrorDecoder 中的 decode 方法,将异常响应包装成自定义异常是常见场景。如果 decode 方法返回的是 RetryableException ,将会触发请求重试,Retryer 负责重试。
Retry
默认情况下,只要 Feign 收到了 IOException ,就会触发重试,当 ErrorDecoder 抛出了 RetryableException 时也会触发请求重试。要改变这个默认行为,需要自定义一个 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 则不触发。
重试后仍然失败则会抛出 RetryException 。可以利用 exceptionPropagationPolicy() 抛出重试失败的根源。
Metrics
Static and Default Methods
Interface 中不仅能声明 API 方法,还可以定义 static 方法和 default 方法(JDK 1.8+)。static 方法可以定义 feign client 的公共配置;default 方法可以用来组装查询对象、定义 API 方法的默认参数值。
Interface GitHub {
// API 方法一
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
// API 方法二
@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");
}
public static void main(String[] args) {
// 是这样写,还是 .repos("Mike", null) 这样写?
// 调用 API 方法二,该方法有 2 个参数,只传了第一参数,第 2 个参数是空值,feign 会调用 default repos 方法自动填充参数
List<Repo> allReposOfMike = GitHub.connect().repos("Mike");
}
}
通过 CompletableFuture支持异步请求
Feign 10.8 中新增了 AsyncFeign ,允许 Interface 中的 API 方法返回 CompletableFuture 类型的实例。
Interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
CompletableFuture<List<Contributor>> contributors(@Param("owner") String owner, @Param("repo") String repo);
}
public class MyApp {
public static void main(String... args) {
GitHub github = AsyncFeign.asyncBuilder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
// Fetch and print a list of the contributors to this library.
CompletableFuture<List<Contributor>> contributors = github.contributors("OpenFeign", "feign");
for (Contributor contributor : contributors.get(1, TimeUnit.SECONDS)) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}
Feign 提供了 2 个异步 client 实现:
- AsyncClient.Default
- AsyncApacheHttp5Client
网友评论