[享学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虽然基于接口实现,但它对接口是有要求的

  1. 不能是泛型接口
  2. 接口最多只能有一个父接口(当然可以没有父接口)

然后他会处理所有的接口方法,包含父接口的。但不包含接口里的默认方法、私有方法、静态方法等,也排除掉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请求,并且解析结果。它的步骤我尝试总结如下:

  1. 通过方法参数,使用工厂构建出一个RequestTemplate请求模版
    1. 这里会解析@RequestLine/@Param等等注解
  2. 从方法参数里拿到请求选项:Options(当然参数里可能也没有此类型,那就是null喽。如果是null,那最终执行默认的选项)
  3. executeAndDecode(template, options)执行发送Http请求,并且完成结果解码(包括正确状态码的解码和错误解码)。这个步骤比较复杂,拆分为如下子步骤:
    1. 把请求模版转换为请求对象feign.Request
      1. 执行所有的拦截器RequestInterceptor,完成对请求模版的定制
      2. 调用目标target,把请求模版转为Request:target.apply(template);
    2. 发送Http请求:client.execute(request, options),得到一个Response对象(这里若发生IO异常,也会被包装为RetryableException重新抛出)
    3. 解析此Response对象,解析后return(返回Object:可能是Response实例,也可能是decode解码后的任意类型)。大致会有如下情况:
      1. Response.class == metadata.returnType(),也就是说你的方法返回值用的就是Response。若response.body() == null,也就是说服务端是返回null/void的话,就直接return response;若response.body().length() == null,就直接返回response;否则,就正常返回response.toBuilder().body(bodyData).build() body里面的内容吧
      2. 200 <= 响应码 <= 300,表示正确的返回。那就对返回值解码即可:decoder.decode(response, metadata.returnType())(解码过程中有可能异常,也会被包装成FeignException向上抛出)
      3. 若响应码是404,并且decode404 = true,那同上也同样执行decode动作
      4. 其它情况(4xx或者5xx的响应码),均执行错误编码:errorDecoder.decode(metadata.configKey(), response)
  4. 发送http请求若一切安好,那就结束了。否则执行重试逻辑:
    1. 通过retryer.continueOrPropagate(e);看看收到此异常后是否要执行重试机制
    2. 需要重试的话就continue(注意上面是while(true)哦~)
    3. 若不需要重试(或者重试次数已到),那就重新抛出此异常,向上抛出
    4. 处理此异常,打印日志…

我个人认为,这是Feign作为一个HC最为核心的逻辑,请各位读者务必掌握


总结

关于Feign的核心API部分就介绍到这里,虽然枯燥但和知识点很硬核,只要啃下来必能有所获。 虽然核心API不可能100%全部讲到,但大部分均已cover,对后续的学习几无障碍,欢迎一起来造…

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭