[享学Feign] 四、原生Feign的核心API详解(二):Contract、SynchronousMethodHandler…
- 2020 年 2 月 21 日
- 筆記
面试中会问,笔试中会考,工作中要用的知识点如果你不掌握,那么面试一般都是到此一游,定级自己为API调用工程师即可。
代码下载地址:https://github.com/f641385712/feign-learning
前言
本文接着上篇文章,接着介绍Feign的核心API部分。革命尚未统一,同志仍需努力。
正文
Logger
feign.Logger
是Feign自己抽象出来的一个日志记录器,定位为调试用的简单日志抽象。
public abstract class Logger { // 记录日志时,对configKey进行格式化 子类可以复写这种实现,但一般都没必要 protected static String methodTag(String configKey) { return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('('))) .append("] ").toString(); } // 它是个protected的抽象方法,子类必须复写。三大参数交给你,子类决定如何书写(写控制台or写文件?) // 比如你可以使用System.out,可以用Log4j、SLF4j都可以... protected abstract void log(String configKey, String format, Object... args); // 记录请求信息。你可以复写,但一般没必要。保持出厂格式吧... protected void logRequest(String configKey, Level logLevel, Request request) { // 打印请求URL... log(configKey, "---> %s %s HTTP/1.1", request.httpMethod().name(), request.url()); // 接下来的内容,只有日志级别在HEADERS以及以上才输出 // 这是Feign自己的日志级别:NONE,BASIC,HEADERS,FULL // 所以只有日志级别为HEADERS,FULL下面才会输出,日志级别是你定的。但默认是NONE if (logLevel.ordinal() >= Level.HEADERS.ordinal()) { ... // 除数请求头、请求体、响应码、响应体等 } } // 记录响应信息 protected Response logAndRebufferResponse(...) { ... } // 记录异常信息 protected IOException logIOException(...){ ... } }
综上可以看到,其实留给子类实现的抽象方法有且仅有一个:log()
方法。Feign自己内置了三个实现类:
ErrorLogger
:极其简单,仅实现了log()方法,使用错误流System.err.printf(methodTag(configKey) + format + "%n", args)
直接删除NoOpLogger
:实现了相关方法,全部为空实现。这是Feign默认的日志记录器JavaLogger
:基于JDK自己的java.util.logging.Logger logger
去记录日志,但是,但是,但是JDK自己的日志级别必须在FINE以上才会进行记录,而它默认的级别是INFO,所以FINE、DEBUG等都不会输出- 你若想JDK的这个日志器输出,你得通过配置文件等放置改变对应实例的日志级别,相对麻烦,生产环境其实一般不用它,后面会讲和SLF4j的集成。
InvocationHandlerFactory
控制反射方法调度。它是一个工厂,用于为目标target创建一个java.lang.reflect.InvocationHandler
反射调用对象。
public interface InvocationHandlerFactory { // Dispatcher:每个方法对应的MethodHandler -> SynchronousMethodHandler实例 // 创建出来的是一个FeignInvocationHandler实例,实现了InvocationHandler接口 InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch); }
Default
它有且仅有一个实现类:feign.InvocationHandlerFactory.Default
。
static final class Default implements InvocationHandlerFactory { // 很简单:调用FeignInvocationHandler构造器完成实例的创建 @Override public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) { return new ReflectiveFeign.FeignInvocationHandler(target, dispatch); } }
创建InvocationHandler
的实例很简单,直接调用FeignInvocationHandler
的构造器完成。但是很有必要细读它的invoke方法,它是对方法完成正调度的核心,是所有方法调用的入口。
FeignInvocationHandler
它实现了InvocationHandler
接口,用于调度所有的Method。
static class FeignInvocationHandler implements InvocationHandler { private final Target target; private final Map<Method, MethodHandler> dispatch; ... @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { ... //省略对equals、hashCode、toString等方法的处理代码 // 通过dispath完成调度,因此最终调用的是MethodHandler#invoke return dispatch.get(method).invoke(args); } }
可以看到该invoke方法仅仅是调度了一下而已,最终实际是委托给SynchronousMethodHandler#invoke(args)
去完成实际的调用:发送http请求 or 调用接口本地方法。
说明:
SynchronousMethodHandler
是整个Feign核心流程的重中之重,我把它放在文末着重讲解分析
Contract
这个接口非常重要:它决定了哪些注解可以标注在接口/接口方法上是有效的,并且提取出有效的信息,组装成为MethodMetadata
元信息。
public interface Contract { // 此方法来解析类中链接到HTTP请求的方法:提取有效信息到元信息存储 // MethodMetadata:方法各种元信息,包括但不限于 // 返回值类型returnType // 请求参数、请求参数的index、名称 // url、查询参数、请求body体等等等等 List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType); }
该接口的继承结构如下图:

BaseContract
抽象基类。
abstract class BaseContract implements Contract { @Override public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) { // 这些检查挺有意思的 // 1、类上不能存在任何一个泛型变量 ... targetType.getTypeParameters().length == 0 // 2、接口最多最多只能有一个父接口 ... targetType.getInterfaces().length <= 1 targetType.getInterfaces()[0].getInterfaces().length == 0 // 对该类所有的方法进行解析:包装成一个MethodMetadata // getMethods表示本类 + 父类的public方法 // 因为是接口,所有肯定都是public的(当然Java8支持private、default、static等) Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>(); for (Method method : targetType.getMethods()) { ... // 排除掉Object的方法、static方法、default方法等 // parseAndValidateMetadata是本类的一个protected方法 MethodMetadata metadata = parseAndValidateMetadata(targetType, method); // 请注意这个key是:metadata.configKey() result.put(metadata.configKey(), metadata); // 注意:这里并没有把result直接返回回去 // 而是返回一个快照版本 return new ArrayList<>(result.values()); } } }
从此处可以清楚的看到,Feign虽然基于接口实现,但它对接口是有要求的:
- 不能是泛型接口
- 接口最多只能有一个父接口(当然可以没有父接口)
然后他会处理所有的接口方法,包含父接口的。但不包含接口里的默认方法、私有方法、静态方法等,也排除掉Object里的方法。 而对方法的到元数据的解析,落在了parseAndValidateMetadata()
这个protected方法上:
BaseContract: protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) { MethodMetadata data = new MethodMetadata(); // 方法返回类型是支持泛型的 data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType())); // 这里使用了Feign的一个工具方法,来生成configKey,不用过于了解细节,简单的说就是尽量唯一 data.configKey(Feign.configKey(targetType, method)); // 这一步很重要:处理接口上的注解。并且处理了父接口哦 // 这就是为何你父接口上的注解,子接口里也生效的原因哦~~~ // processAnnotationOnClass()是个abstract方法,交给子类去实现(毕竟注解是可以扩展的嘛) if (targetType.getInterfaces().length == 1) { processAnnotationOnClass(data, targetType.getInterfaces()[0]); } processAnnotationOnClass(data, targetType); // 处理标注在方法上的所有注解 // 若子接口override了父接口的方法,注解请以子接口的为主,忽略父接口方法 for (Annotation methodAnnotation : method.getAnnotations()) { processAnnotationOnMethod(data, methodAnnotation, method); } // 简单的说:处理完方法上的注解后,必须已经知道到底是GET or POST 或者其它了 checkState(data.template().method() != null, // 方法参数,支持泛型类型的。如List<String>这种... Class<?>[] parameterTypes = method.getParameterTypes(); Type[] genericParameterTypes = method.getGenericParameterTypes(); // 注解是个二维数组... Annotation[][] parameterAnnotations = method.getParameterAnnotations(); int count = parameterAnnotations.length; // 一个注解一个注解的处理 for (int i = 0; i < count; i++) { ... // processAnnotationsOnParameter是抽象方法,子类去决定 if (parameterAnnotations[i] != null) { isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i); } // 方法参数若存在URI类型的参数,那url就以它为准,并不使用全局的了 if (parameterTypes[i] == URI.class) { data.urlIndex(i); } ... // 校验body: // 1、body参数不能用作form表单的parameters // 2、Body parameters不能太多 ... return data; } }
按照从上至下流程解析每一个方法上的注解信息,抽象方法留给子类具体实现:
- processAnnotationOnClass:
- processAnnotationOnMethod:
- processAnnotationsOnParameter:
这三个抽象方法非常的公用,决定了识别哪些注解、解析哪些注解,因此特别适合第三方扩展。 显然,Feign内置的默认实现实现了这些接口,就连Spring MVC的扩展SpringMvcContract
也是通过继承它来实现支持@RequestMapping
等注解的。
Default
它是BaseContract
的内置唯一实现类,也是Feign的默认实现。
class Default extends BaseContract { static final Pattern REQUEST_LINE_PATTERN = Pattern.compile("^([A-Z]+)[ ]*(.*)$"); // 支持注解:@Headers @Override protected void processAnnotationOnClass(MethodMetadata data, Class<?> targetType) { ... } // 支出注解:@RequestLine、@Body、@Headers @Override protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) { ... } // 支持注解:@Param、@QueryMap、@HeaderMap等 @Override protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) { ... } ... }
通过这个子类实现,可以很清楚的看到Feign原生支持哪些注解类型,它和上篇文章的介绍是完全吻合的。
MethodHandler
功能上类似于java.lang.reflect.InvocationHandler#invoke()
方法,接口定义非常简单:
interface MethodHandler { Object invoke(Object[] argv) throws Throwable; }
接口定义虽简单,但它是完成Feign发送请求过程的重中之重。该接口有两个内置实现类:

注意:该接口名字叫MethodHandler
,而JDK的叫MethodHandle
,不要弄混了…
DefaultMethodHandler
它是通过调用接口默认方法(因为它并不持有Target
这种代理对象,所以接口中能用的方法只能是默认方法喽)代码来处理方法,注意:bindTo方法必须在invoke方法之前调用。
它依赖的技术是Java7提供的方法句柄MethodHandle
,比反射来得更加的高效,缺点是编码稍显复杂。
说明:关于JDK的方法句柄
MethodHandle
具体如何使用?有兴趣的同学请自行研究
// 访问权限是Default final class DefaultMethodHandler implements MethodHandler { private final MethodHandle unboundHandle; private MethodHandle handle; // 从Method里拿到方法句柄,赋值给unboundHandle public DefaultMethodHandler(Method defaultMethod) { ... } // 把目标对象(代理对象)绑定到方法句柄上 // 这样unboundHandle就变为了已经绑定好的handle,这样invoke就能调用啦 public void bindTo(Object proxy) { if (handle != null) { throw new IllegalStateException("Attempted to rebind a default method handler that was already bound"); } handle = unboundHandle.bindTo(proxy); } // 调用目标方法 // 调用前:请确保已经绑定了目标对象 @Override public Object invoke(Object[] argv) throws Throwable { if (handle == null) { throw new IllegalStateException("Default method handler invoked before proxy has been bound."); } return handle.invokeWithArguments(argv); } }
默认实现的特点是:采用的方法句柄MethodHandle
的方式去“反射”执行目标方法,很显然它只能执行到接口默认方法,所以一般木有远程通信这么一说。
SynchronousMethodHandler(重中之重)
同步方法调用处理器,它强调的是同步二字,且有远程通信。
final class SynchronousMethodHandler implements MethodHandler { // 方法元信息 private final MethodMetadata metadata; // 目标 也就是最终真正构建Http请求Request的实例 一般为HardCodedTarget private final Target<?> target; // 负责最终请求的发送 -> 默认传进来的是基于JDK源生的,效率很低,不建议直接使用 private final Client client; // 负责重试 -->默认传进来的是Default,是有重试机制的哦,生产上使用请务必注意 private final Retryer retryer; // 请求拦截器,它会在target.apply(template); 也就是模版 -> 请求的转换之前完成拦截 // 说明:并不是发送请求之前那一刻哦,请务必注意啦 // 它的作用只能是对请求模版做定制,而不能再对Request做定制了 // 内置仅有一个实现:BasicAuthRequestInterceptor 用于鉴权 private final List<RequestInterceptor> requestInterceptors; // 若你想在控制台看到feign的请求日志,改变此日志级别为info吧(因为一般只有info才会输出到日志文件) private final Logger.Level logLevel; ... // 构建请求模版的工厂 // 对于请求模版,有多种构建方式,内部会用到可能多个编码器,下文详解 private final RequestTemplate.Factory buildTemplateFromArgs; // 请求参数:比如链接超时时间、请求超时时间等 private final Options options; // 解码器:用于对Response进行解码 private final Decoder decoder; // 发生错误/异常时的解码器 private final ErrorDecoder errorDecoder; // 是否解码404状态码?默认是不解码的 private final boolean decode404; // 唯一的构造器,并且还是私有的(所以肯定只能在本类内构建出它的实例喽) // 完成了对如上所有属性的赋值 private SynchronousMethodHandler( ... ) { ... } @Override public Object invoke(Object[] argv) throws Throwable { // 根据方法入参,结合工厂构建出一个请求模版 RequestTemplate template = buildTemplateFromArgs.create(argv); // findOptions():如果你方法入参里含有Options类型这里会被找出来 // 说明:若有多个只会有第一个生效(不会报错) Options options = findOptions(argv); // 重试机制:注意这里是克隆一个来使用 Retryer retryer = this.retryer.clone(); while (true) { try { return executeAndDecode(template, options); } catch (RetryableException e) { // 若抛出异常,那就触发重试逻辑 try { // 该逻辑是:如果不重试了,该异常会继续抛出 // 若要充值,就会走下面的continue retryer.continueOrPropagate(e); } catch (RetryableException th) { ... } continue; } } } }
该MethodHandler
实现相对复杂,用一句话描述便是:准备好所有参数后,发送Http请求,并且解析结果。它的步骤我尝试总结如下:
- 通过方法参数,使用工厂构建出一个
RequestTemplate
请求模版- 这里会解析
@RequestLine/@Param
等等注解
- 这里会解析
- 从方法参数里拿到请求选项:
Options
(当然参数里可能也没有此类型,那就是null喽。如果是null,那最终执行默认的选项) executeAndDecode(template, options)
执行发送Http请求,并且完成结果解码(包括正确状态码的解码和错误解码)。这个步骤比较复杂,拆分为如下子步骤:- 把请求模版转换为请求对象
feign.Request
- 执行所有的拦截器
RequestInterceptor
,完成对请求模版的定制 - 调用目标target,把请求模版转为Request:
target.apply(template);
- 执行所有的拦截器
- 发送Http请求:
client.execute(request, options)
,得到一个Response对象(这里若发生IO异常,也会被包装为RetryableException
重新抛出) - 解析此Response对象,解析后return(返回Object:可能是Response实例,也可能是decode解码后的任意类型)。大致会有如下情况:
Response.class == metadata.returnType()
,也就是说你的方法返回值用的就是Response。若response.body() == null
,也就是说服务端是返回null/void的话,就直接return response
;若response.body().length() == null
,就直接返回response;否则,就正常返回response.toBuilder().body(bodyData).build()
body里面的内容吧- 若
200 <= 响应码 <= 300
,表示正确的返回。那就对返回值解码即可:decoder.decode(response, metadata.returnType())
(解码过程中有可能异常,也会被包装成FeignException
向上抛出) - 若响应码是404,并且
decode404 = true
,那同上也同样执行decode动作 - 其它情况(4xx或者5xx的响应码),均执行错误编码:
errorDecoder.decode(metadata.configKey(), response)
- 把请求模版转换为请求对象
- 发送http请求若一切安好,那就结束了。否则执行重试逻辑:
- 通过
retryer.continueOrPropagate(e);
看看收到此异常后是否要执行重试机制 - 需要重试的话就continue(注意上面是while(true)哦~)
- 若不需要重试(或者重试次数已到),那就重新抛出此异常,向上抛出
- 处理此异常,打印日志…
- 通过
我个人认为,这是Feign作为一个HC最为核心的逻辑,请各位读者务必掌握。
总结
关于Feign的核心API部分就介绍到这里,虽然枯燥但和知识点很硬核,只要啃下来必能有所获。 虽然核心API不可能100%全部讲到,但大部分均已cover,对后续的学习几无障碍,欢迎一起来造…
声明
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。