OpenFeign学习(二):高级用法自定义配置组件HttpClient / SLF4J / RequestInterceptor等

说明

在项目开发中,避免不了通过HTTP请求进行对第三方服务的调用,在上篇博文OkHttp的高级封装Feign学习(一): Feign注解的使用中,我对Feign注解基本使用进行了学习总结。本篇博文我将继续对feign的其他特性及高级用法进行学习总结。

正文

feign具有很强的扩展性,允许用户根据需要进行定制,如HTTP客户端OkHttp, HTTP/2 client, SLF4J日志的使用, 编解码,错误处理等。使用时可以通过Feign.builder()创建api客户端时配置自定义组件。

Encoders & Decoders

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 paramMap形式。

@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,详见文档。

日志 SLF4J

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 : 对请求的请求头,请求体,元数据及响应的响应头,响应体,元数据进行日志输出

OkHttp & HTTP2

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。

继承公共api接口

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/");

重试retry

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

你可能感兴趣的:(Feign学习,java,openfeign,http,okhttp)