面试中会问,笔试中会考,工作中要用的知识点如果你不掌握,那么面试一般都是到此一游,定级自己为API调用工程师即可。
代码下载地址:https://github.com/f641385712/feign-learning
本文接着上篇文章,接着介绍Feign的核心API部分。革命尚未统一,同志仍需努力。
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等都不会输出 控制反射方法调度。它是一个工厂,用于为目标target创建一个java.lang.reflect.InvocationHandler
反射调用对象。
public interface InvocationHandlerFactory {
// Dispatcher:每个方法对应的MethodHandler -> SynchronousMethodHandler实例
// 创建出来的是一个FeignInvocationHandler实例,实现了InvocationHandler接口
InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch);
}
它有且仅有一个实现类: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方法,它是对方法完成正调度的核心,是所有方法调用的入口。
它实现了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核心流程的重中之重,我把它放在文末着重讲解分析
这个接口非常重要:它决定了哪些注解可以标注在接口/接口方法上是有效的,并且提取出有效的信息,组装成为MethodMetadata
元信息。
public interface Contract {
// 此方法来解析类中链接到HTTP请求的方法:提取有效信息到元信息存储
// MethodMetadata:方法各种元信息,包括但不限于
// 返回值类型returnType
// 请求参数、请求参数的index、名称
// url、查询参数、请求body体等等等等
List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
}
该接口的继承结构如下图:
抽象基类。
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;
}
}
按照从上至下流程解析每一个方法上的注解信息,抽象方法留给子类具体实现:
这三个抽象方法非常的公用,决定了识别哪些注解、解析哪些注解,因此特别适合第三方扩展。
显然,Feign内置的默认实现实现了这些接口,就连Spring MVC的扩展SpringMvcContract
也是通过继承它来实现支持@RequestMapping
等注解的。
它是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原生支持哪些注解类型,它和上篇文章的介绍是完全吻合的。
功能上类似于java.lang.reflect.InvocationHandler#invoke()
方法,接口定义非常简单:
interface MethodHandler {
Object invoke(Object[] argv) throws Throwable;
}
接口定义虽简单,但它是完成Feign发送请求过程的重中之重。该接口有两个内置实现类:
注意:该接口名字叫MethodHandler
,而JDK的叫MethodHandle
,不要弄混了…
它是通过调用接口默认方法(因为它并不持有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
的方式去“反射”执行目标方法,很显然它只能执行到接口默认方法,所以一般木有远程通信这么一说。
同步方法调用处理器,它强调的是同步二字,且有远程通信。
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.apply(template);
client.execute(request, options)
,得到一个Response对象(这里若发生IO异常,也会被包装为RetryableException
重新抛出)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
向上抛出)decode404 = true
,那同上也同样执行decode动作errorDecoder.decode(metadata.configKey(), response)
retryer.continueOrPropagate(e);
看看收到此异常后是否要执行重试机制我个人认为,这是Feign作为一个HC最为核心的逻辑,请各位读者务必掌握。
关于Feign的核心API部分就介绍到这里,虽然枯燥但和知识点很硬核,只要啃下来必能有所获。 虽然核心API不可能100%全部讲到,但大部分均已cover,对后续的学习几无障碍,欢迎一起来造…
原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭
。