前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[享学Feign] 四、原生Feign的核心API详解(二):Contract、SynchronousMethodHandler...

[享学Feign] 四、原生Feign的核心API详解(二):Contract、SynchronousMethodHandler...

作者头像
YourBatman
发布2020-02-21 16:27:09
3.7K0
发布2020-02-21 16:27:09
举报
文章被收录于专栏:BAT的乌托邦

面试中会问,笔试中会考,工作中要用的知识点如果你不掌握,那么面试一般都是到此一游,定级自己为API调用工程师即可。

代码下载地址:https://github.com/f641385712/feign-learning

前言

本文接着上篇文章,接着介绍Feign的核心API部分。革命尚未统一,同志仍需努力。


正文

Logger

feign.Logger是Feign自己抽象出来的一个日志记录器,定位为调试用的简单日志抽象。

代码语言:javascript
复制
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反射调用对象。

代码语言:javascript
复制
public interface InvocationHandlerFactory {
	
	// Dispatcher:每个方法对应的MethodHandler -> SynchronousMethodHandler实例
	// 创建出来的是一个FeignInvocationHandler实例,实现了InvocationHandler接口
	InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch);
}

Default

它有且仅有一个实现类:feign.InvocationHandlerFactory.Default

代码语言:javascript
复制
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。

代码语言:javascript
复制
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元信息

代码语言:javascript
复制
public interface Contract {

	// 此方法来解析类中链接到HTTP请求的方法:提取有效信息到元信息存储
	// MethodMetadata:方法各种元信息,包括但不限于
	// 返回值类型returnType
	// 请求参数、请求参数的index、名称
	// url、查询参数、请求body体等等等等
	List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
}

该接口的继承结构如下图:

在这里插入图片描述
在这里插入图片描述

BaseContract

抽象基类。

代码语言:javascript
复制
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方法上:

代码语言:javascript
复制
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的默认实现

代码语言:javascript
复制
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()方法,接口定义非常简单:

代码语言:javascript
复制
interface MethodHandler {
	Object invoke(Object[] argv) throws Throwable;
}

接口定义虽简单,但它是完成Feign发送请求过程的重中之重。该接口有两个内置实现类:

在这里插入图片描述
在这里插入图片描述

注意:该接口名字叫MethodHandler,而JDK的叫MethodHandle,不要弄混了…


DefaultMethodHandler

它是通过调用接口默认方法(因为它并不持有Target这种代理对象,所以接口中能用的方法只能是默认方法喽)代码来处理方法,注意:bindTo方法必须在invoke方法之前调用。

它依赖的技术是Java7提供的方法句柄MethodHandle,比反射来得更加的高效,缺点是编码稍显复杂。

说明:关于JDK的方法句柄MethodHandle具体如何使用?有兴趣的同学请自行研究

代码语言:javascript
复制
// 访问权限是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(重中之重)

同步方法调用处理器,它强调的是同步二字,且有远程通信。

代码语言:javascript
复制
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,对后续的学习几无障碍,欢迎一起来造…

声明

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • Logger
      • InvocationHandlerFactory
        • FeignInvocationHandler
          • Contract
            • MethodHandler
            • 总结
              • 声明
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档