在项目开发中,避免不了通过HTTP请求进行对第三方服务的调用,在上篇博文OkHttp的高级封装Feign学习(一): Feign注解的使用中,我对Feign注解基本使用进行了学习总结。本篇博文我将继续对feign的其他特性及高级用法进行学习总结。
feign具有很强的扩展性,允许用户根据需要进行定制,如HTTP客户端OkHttp, HTTP/2 client, SLF4J日志的使用, 编解码,错误处理等。使用时可以通过Feign.builder()创建api客户端时配置自定义组件。
feign支持使用Gson和Jackson作为编解码工具,在创建api接口时通过encoder()和decoder()方法分别指定。在使用不同的类型时,需要引入不同的依赖 feign-jackson 或 feign-gson 。
在feign中,默认的解码器只能解码类型为Response, String, byte[], void的返回值,若返回值包含其他类型,就必须指定解码器Decoder。并且希望在解码前对返回值进行前置处理,则需要使用mapAndDecode方法,设置处理逻辑,使用示例详见文档。
同时,在发送请求时使用的post方法,方法参数为String或者byte[],并且参数没有使用注解,这时就需要添加Content-Type请求头并通过encoder()方法设置编码器,来发送类型安全的请求体。
编码示例:
在请求时,设置Content-Type为application/json,方法参数为一个pojo,并指定encoder,这里使用jackson。
io.github.openfeign
feign-jackson
10.7.4
@RequestLine("POST /test/json/encoder")
@Headers("Content-Type: application/json")
ResultPojo jsonEncoderTest(JSONObject content);
HelloService service = Feign.builder()
.client(new OkHttpClient())
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(HelloService.class, "http://localhost:8080/");
服务端接口又如何接收该类型的参数?可以使用@RequestBody将json格式参数转换为pojo或Map
@RequestMapping("/test/json/encoder")
public String jsonEncoder(@RequestBody ParamPojo paramPojo) {
System.out.println(paramPojo.getName() + "-" + paramPojo.getPassword() + "- " + paramPojo.getRoles().size());
JSONObject object = new JSONObject();
object.put("code", 0);
object.put("msg", "success");
return object.toJSONString();
}
解码示例:在一般的http请求中,返回的多为json格式的字符串,在使用时需要解析为json对象,我们可以通过设置decoder,直接将结果转为对应的对象。
public class ResultPojo {
private Integer code;
private String msg;
....省略set get
}
@RequestLine("GET /test/json/decoder")
ResultPojo jsonDecoderTest();
仍然是在Feign.builder()方法中通过decoder()设置对应的组件。
同样的,Feign也支持对xml格式的编解码,支持Sax 和 JAXB,详见文档。
feing支持对http请求响应记录日志,在创建api接口时通过logger()来配置logger,通过logLevel()设置日志等级。并且支持了SLF4J,方便与系统使用日志进行整合。
这里我使用log4j2进行示例:
排除springboot自带依赖,添加log4j2和feign-slf4j依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-log4j2
io.github.openfeign
feign-slf4j
10.7.4
log4j的配置:
注意,在配置文件需要配置单独的logger,并且name为feign.Logger,level为debug。
在创建api接口时feign.builder()进行配置。
HelloService service = Feign.builder()
.logger(new Slf4jLogger())
.logLevel(Logger.Level.FULL)
.client(new OkHttpClient())
.target(HelloService.class, "http://localhost:8080/");
注意,loglevel一共有四个等级,分别为NONE, BASIC, HEADERS, FULL
NONE : 没有日志输出
BASIC : 只对请求方法,url和响应状态进行日志输出
HEADERS : 对请求和响应的请求头、响应头的基本信息进行日志输出
FULL : 对请求的请求头,请求体,元数据及响应的响应头,响应体,元数据进行日志输出
feign支持通过OkHttpClient或Http2Client发送http请求。
OkHttpClient将feign的http请求定向到okhttp,okhttp支持SPDY协议和更好的网络控制。
Http2Client将请求定向到java11的NEW HTTP/2 Client, 该客户端基于HTTP/2协议。注意,要使用该客户端,需要java sdk 11。
有关SPDY, HTTP/2更多内容详见 一文读懂 HTTP/1HTTP/2HTTP/3。
feign支持将公共api抽取到一个公共接口,支持公共api接口的单一继承。
示例:
public interface CommonService {
@RequestLine("GET /test/hello2?name={name}")
ResultPojo common(@Param("name") String name);
}
public interface HelloService extends CommonService{
}
HelloService service = Feign.builder()
.logger(new Slf4jLogger())
.logLevel(Logger.Level.FULL)
.client(new OkHttpClient())
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(HelloService.class, "http://localhost:8080/");
@Test
public void commonServiceTest() {
ResultPojo res = service.common("tom");
}
在上篇有关注解的使用一文中,我们已经了解到了对于请求头的设置我们可以使用@Headers和@HeaderMap注解,其中@Headers注解既可以使用在方法上也可以使用在接口类(Type)上。
因此,当我们需要对一个请求目标(Target)的每个方法都设置请求头时,可以使用@Headers注解。但是,如果需要对不同的请求目标的所有方法都设置请求头,这时就需要使用请求拦截器。请求拦截器可以在不同的请求目标实例(feign.builder创建的客户端)间共享,并且是线程安全的。
RequestInterceptors can be shared across Target instances and are expected to be thread-safe. RequestInterceptors are applied to all request methods on a Target.
示例:
为所有请求方法都设置请求头user-agent值:
public class HeadersInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("user-agent", "wds");
}
}
HelloService service = Feign.builder()
.client(new OkHttpClient())
.requestInterceptor(new HeadersInterceptor())
.target(HelloService.class, "http://localhost:8080/");
如果想对每个方法都进行定制,那就需要创建一个定制的target。因为拦截器没有权限获取到方法的元数据。详情示例请看官方文档的 Setting headers per target 部分。
feign支持对意外的请求响应做自定义处理,所有的不成功(HTTP status 不为2xx)的响应都会触发ErrorDecoder的decode()方法进行处理,允许自定义异常或进行其他处理。如果希望进行重试再次请求,则需要抛出RetryableException异常,此时就会调用注册在客户端的Retryer。
示例:对404的响应抛出自定义异常
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String s, Response response) {
int code = response.status();
if (code == 404) {
return new RuntimeException(s + "请求方法不存在");
}
return null;
}
}
HelloService service = Feign.builder()
.errorDecoder(new CustomErrorDecoder())
.target(HelloService.class, "http://localhost:8080/");
feign默认会对所有HTTP方法请求抛出的IOException和ErrorDecoder抛出的RetryableException异常的请求进行重试。 自定义重试频率策略可以创建定制的Retryer,一定要重写clone()方法,否则会抛出NullException。
重试机制:
Retryer根据continueOrPropagate(RetryableException e)方法返回的true或false决定是否重试。
以上是官方文档说明的重试机制,但是通过阅读源码发现,feign自身有一个实现Retryer接口的内部类Default,并且接口的方法continueOrPropagate(RetryableException var1)无返回值,并且Default实现该方法控制了重试频率。所以重试并不是根据此方法返回的true或false决定。
public void continueOrPropagate(RetryableException e) {
if (this.attempt++ >= this.maxAttempts) {
throw e;
} else {
long interval;
if (e.retryAfter() != null) {
interval = e.retryAfter().getTime() - this.currentTimeMillis();
if (interval > this.maxPeriod) {
interval = this.maxPeriod;
}
if (interval < 0L) {
return;
}
} else {
interval = this.nextMaxInterval();
}
try {
Thread.sleep(interval);
} catch (InterruptedException var5) {
Thread.currentThread().interrupt();
throw e;
}
this.sleptForMillis += interval;
}
}
每个客户端都需要有自己的Retryer实例,并且允许Retryer维护每个请求的状态。
为了保证每个客户端都有自己的Retryer示例,在执行方法时,都会调用注册retryer对象的clone()方法,在默认实现Default类中,clone()方法创建自身新实例返回。
public Object invoke(Object[] argv) throws Throwable {
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Options options = this.findOptions(argv);
Retryer retryer = this.retryer.clone();
.....
}
public static class Default implements Retryer {
.....
public Retryer clone() {
return new Retryer.Default(this.period, this.maxPeriod, this.maxAttempts);
}
}
当重试失败时,最后的RetryException异常将会被抛出。如果需要抛出导致重试失败的原始原因,则需要在创建客户端是设置exceptionPropagationPolicy()配置。通过源码看出,当配置为ExceptionPropagationPolicy.UNWRAP时,会返回原始的cause。
try {
retryer.continueOrPropagate(e);
} catch (RetryableException var8) {
Throwable cause = var8.getCause();
if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
throw cause;
}
throw var8;
}
示例:创建自定义Retryer
注意,默认情况下feign只会对IOException进行重试,如果需要其他情况的重试,则需要创建配置自定义的ErrorDecoder,抛出RetryableException异常。这里,我仍使用之前的CustomErrorDecoder :
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String s, Response response) {
int code = response.status();
if (code == 404) {
throw new RetryableException(code, "Service Unavailable", response.request().httpMethod(), null, response.request());
}
return null;
}
}
MyRetryer :
注意 最大重试次数的设置,在默认重试器Default中,默认的最大重试次数为5,包含了第一次请求失败的动作,所以失败后的实际重试次数是4次。
public class MyRetryer implements Retryer {
private final int maxAttempts;
int attempt;
public MyRetryer(int maxAttempts) {
this.maxAttempts = maxAttempts;
this.attempt = 1;
}
@Override
public void continueOrPropagate(RetryableException e) {
if (this.attempt++ >= this.maxAttempts) {
throw e;
} else {
System.out.println("重试 " + attempt + " 次");
try {
Thread.sleep(1000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
ex.printStackTrace();
}
}
}
@Override
public Retryer clone() {
return new MyRetryer(5);
}
}
配置Retryer :
HelloService service = Feign.builder()
.retryer(new MyRetryer(5))
.exceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP)
.target(HelloService.class, "http://localhost:8080/");
使用建议:除非需要重新定制重试频率策略,否则建议使用默认的重试器Default。
HelloService service = Feign.builder()
.retryer(new Retryer.Default(100, 1000, 5))
.exceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP)
.target(HelloService.class, "http://localhost:8080/");
feign还支持Ribbon和Hystrix,他们的使用方式以及api接口中的静态和默认方法的使用方式,请看官方文档。
至此,feign的一些高级用法已经学习介绍完毕,其中常用的包括了encoder, decoder, okhttpclient, slf4j等。接下来我将继续通过源码学习总结feign运行的原理。
示例源码地址:https://github.com/Edenwds/javaweb/tree/master/feigndemo
参考资料:
https://github.com/OpenFeign/feign
https://mp.weixin.qq.com/s/fy84edOix5tGgcvdFkJi2w
https://stackoverflow.com/questions/56987701/feign-client-retry-on-exception