[享学Feign] 八、Feign是如何生成接口代理对象的?Feign实例的构建器Feign.Builder详解

  • 2020 年 2 月 21 日
  • 筆記

如果你觉得自己很重要,就不要把你的API设计得很粗糙,一是要稳定,二是要非常简单好用。

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

前言

一步步稳扎稳打,终于到了该研究平时接触的API:feign.Feign本身。 feign.Feign它是最上层的API,是使用者直接使用的对象,它能完成对接口生成动态代理对象,从而很方面的让你面向接口编程,而不用太过例会Feign内部的实现细节。

如果说前面七篇都是在打基础,那么它们均是在帮你理解这篇的内容而服务。那么作为最重要的一步:生成接口代理对象有何内幕?以及平时我们用于构建Feign的构建器Feign.Builder有何文章可循,本篇文章将彻底为你揭秘,帮你完整掌握Feign的核心内容。

说明:本文是Feign核心内容的最后一篇,接下来的内容将聚焦在扩展、集成、定制方面。


正文

作为Feign核心内容的最后一文,本文将聚焦在Feign实例本身,从源码深处讲解它的实现内幕。


feign.Feign

Feign的目的是简化针对rest的Http Api的开发。在实现中,Feign是一个用于生成目标实例Feign#newInstance()的工厂,这个生成的实例便是接口的代理对象

该类是个抽象类:

public abstract class Feign {    	// 唯一的public的抽象方法,用于为目标target创建一个代理对象实例  	public abstract <T> T newInstance(Target<T> target);    	// -----------------静态方法-----------------    	// 工具方法,生成configKey  	// MethodMetadata#configKey属性的值就来自于此方法  	public static String configKey(Class targetType, Method method) { ... }    	// Feign的实例统一,有且只能通过builder构建,文下有详解,它是重点  	public static Builder builder() {  	  return new Builder();  	}  }

feign.Feign它有且仅有一个唯一实现类,但这个实现类并不面向使用者,使用者只需用Builder构建,面向接口/抽象类编程足矣。但作为本专栏的定位,还需弄清楚它的来龙去脉,迎难而上吧~


ParseHandlersByName

在接触ReflectiveFeign之前,我认为很有必要了解下它。 该类有且仅提供一个方法:将feign.Target转为Map<String, MethodHandler>,就是说为每一个configKey(其实就是一个Method),找到一个处理它的MethodHandler

题外话:我个人感觉,相较于Spring家族,Feign的各种取名确实有点随意,有点任性。。。

static final class ParseHandlersByName {    	// 提取器      private final Contract contract;      private final Options options;        private final Encoder encoder;      private final Decoder decoder;      private final ErrorDecoder errorDecoder;      private final QueryMapEncoder queryMapEncoder;    	// 这个工厂只产生SynchronousMethodHandler实例  	// 从这也很好理解:所有的Method最终的处理器都是SynchronousMethodHandler      private final SynchronousMethodHandler.Factory factory;  	... // 省略构造器      	public Map<String, MethodHandler> apply(Target key) {  		// 通过Contract提取出该类所有方法的元数据信息:MethodMetadata  		// 它会解析注解,不同的实现支持的注解是不一样的  		List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());    	  // 一个方法一个方法的处理,生成器对应的MethodHandler处理器  	  // 上篇文章有讲过,元数据都是交给RequestTemplate.Factory去构建成为一个请求模版的        Map<String, MethodHandler> result = new LinkedHashMap<>();        for (MethodMetadata md : metadata) {  		// 这里其实我觉得使用接口RequestTemplate.Factory buildTemplate更加的合适  		BuildTemplateByResolvingArgs buildTemplate;    		// 针对不同元数据参数,调用不同的RequestTemplate.Factory实现类完成处理  		if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {  			// 若存在表单参数formParams,并且没有body模版,那就执行表单形式的构建  			buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder);  		} else if (md.bodyIndex() != null) {  			// 若存在body,那就是body喽  			buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder);  		} else {  			// 否则就是普通形式:查询参数构建方式  			buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder);  		}    		// 通过factory.create创建出MethodHandler实例,缓存结果  		result.put(md.configKey(), factory.create(...));  		return result;  	}  }

步骤、过程介绍都标注在源码处了,一句话总结ParseHandlersByName的作用:为指定接口类型的每个方法生成其对应的MethodHandler处理器(可能是默认方法直接执行处理、也可能是发送http请求去处理)。

该步骤中,涉及到元数据提取、编码、模版数据填充等动作,均交给不同的组件去完成,组件化的设计有助于模块化、可插拔等特性。


ReflectiveFeign

Reflective中文释义:反射的。 它是feign.Feign的唯一实现,但是它并不提供public的构造器,因此外部并不能直接构建它。

public class ReflectiveFeign extends Feign {      // 文上有解释次类的作用:提供方法,给接口每个方法生成一个处理器    private final ParseHandlersByName targetToHandlersByName;    // 它是调度中心    private final InvocationHandlerFactory factory;    private final QueryMapEncoder queryMapEncoder;      ... // 省略构造器  }

准备好几大“工具”后,下面开始干活了:实现父类newInstance()抽象方法,给Target<T>生成一个实例(实为代理对象)返回。

ReflectiveFeign:      @Override    public <T> T newInstance(Target<T> target) {    	// 拿到该接口所有方法对应的处理器的Map    	Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);    	// 真要处理调用的Method对应的处理器Map  	Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<>();  	// 简单的说:对接口默认方法作为处理方法提供支持,不用发http请求喽,一般可忽略  	List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<>();    	// 查阅该接口所有的Method。  	// .getMethods()会获取接口的所有的public方法,包括default方法哦(因为defualt方法也是public的)  	for (Method method : target.type().getMethods()) {  		... //如果是Object的方法,直接continue。method.getDeclaringClass() == Object.class  		... // 如果是Defualt默认方法,那该方法就用DefaultMethodHandler去处理  		... // 否则,就nameToHandler.get(Feign.configKey(target.type(), method))用它去处理  	}    	// 为该目标接口类型创建一个InvocationHandler  	// 它持有methodToHandler这个Map,负责全局调度所有的Method方法  	// 并且为此接口创建一个代理对象  	InvocationHandler handler = factory.create(target, methodToHandler);  	T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler);    	// 根据前文知道:要想调用DefaultMethodHandler#invoke之前,必须先bind  	// 因此这里千万别忘了来一次bind绑定动作      for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {        defaultMethodHandler.bindTo(proxy);      }      return proxy;    }

就这样,通过ReflectiveFeign的newInstance方法给该接口创建出来了一个代理实例,它使用的处理器是FeignInvocationHandler

说明:关于FeignInvocationHandler如何完成全局调度,本专栏第三篇文章就已讲解。

另外想强调一点:ReflectiveFeign为接口生成动态代理实例的步骤比较重要,我把相关说明都备注在源码处,希望同学自己可以总结一份文字步骤出来,学以致用。


feign.Feign.Builder

了解了ReflectiveFeign的作用,它作为feign.Feign的唯一实现,但其实我们在使用过程中几乎不会使用它和接触它,因为构建实例均通过工厂来进行,这便是feign.Feign.Builder

public static class Builder {    	// 请求模版的拦截器,默认的空的,木有哦,你可以自定义,在builder的时候加进来  	private final List<RequestInterceptor> requestInterceptors = new ArrayList<>();    	// 这是Feign自己的日志级别,默认不输出日志  	// 因此若你要打印请求日志,这里级别需要调高  	private Logger.Level logLevel = Logger.Level.NONE;  	// 默认使用的日志记录器,也是不作为  	private Logger logger = new NoOpLogger();    	// 默认使用的提取器,就是支持@RequestLine源生注解的这种  	private Contract contract = new Contract.Default();  	// 默认使用JDK的`HttpURLConnection`发送请求,并且是非SSL加密的  	private Client client = new Client.Default(null, null);    	// 请务必注意:默认情况下Feign是开启了重试的  	// 100ms重试一次,一共重试5次。最长持续1s钟  	// 在生产环境下,重试请务必慎用  	private Retryer retryer = new Retryer.Default();  	// 默认10s链接超时,60s读取超时  	private Options options = new Options();    	// 默认编码器:只支持String类型的编码  	// 请注意:编码器的生效时机哦~~~(没有标注@Param注解会交给编码器处理)      private Encoder encoder = new Encoder.Default();      // 默认只能解码String类型和字节数组类型      private Decoder decoder = new Decoder.Default();    	// 支持把@QueryMap标注在Map or Bean前面  	// Bean需要有public的get方法才算一个属性  	private QueryMapEncoder queryMapEncoder = new QueryMapEncoder.Default();  	// 把异常、错误码包装为FeignException异常向上抛出  	private ErrorDecoder errorDecoder = new ErrorDecoder.Default();    	// FeignInvocationHandler是它唯一的实现  	private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default();    	// 默认不会解码404  	private boolean decode404;  	private boolean closeAfterDecode = true;  	// 异常传播策略:NONE代表不包装 不处理,直接抛  	private ExceptionPropagationPolicy propagationPolicy = NONE;  }

这个Builder构建器的内容非常丰富,是对前面讲解几乎所有组件的一个总结

我们设计一个程序希望它对外是可扩展的,因此通过Builder暴露出这些钩子是完全合理的,当然只有你真正理解了这些组件的作用,你才能在定制上做到游刃有余。

说明:源码上的标注我都有简要的说明,欲知详细,请参考本专栏的第三篇和第四篇文章。

Builder的方法效果同set方法,没什么太多可说的,但这里我摘出几个,相对重视一下:

Feign.Builder:    	// 这两个方法调用其一,方法二是一的加强版      public Builder decoder(Decoder decoder) {        this.decoder = decoder;        return this;      }      public Builder mapAndDecode(ResponseMapper mapper, Decoder decoder) {        this.decoder = new ResponseMappingDecoder(mapper, decoder);        return this;      }    	// 拦截器一个个添加,可以多次调用添加多个      public Builder requestInterceptor(RequestInterceptor requestInterceptor) {        this.requestInterceptors.add(requestInterceptor);        return this;      }      // 请注意:如果一次性添加多个,那么此方法相当于set方法哦,并不是add      public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) {        this.requestInterceptors.clear();        for (RequestInterceptor requestInterceptor : requestInterceptors) {          this.requestInterceptors.add(requestInterceptor);        }        return this;      }    	// 构建:返回一个ReflectiveFeign实例      public Feign build() {        	...        	return new ReflectiveFeign( ... );      }

创建接口代理实例(代码示例)

有了Builder构建器,这样得到接口的代理实例,就可这么做啦:

@Test  public void fun1() {      Feign feign = Feign.builder().build();      DemoClient client = feign.newInstance(new Target.HardCodedTarget<>(DemoClient.class, "http://localhost:8080"));      System.out.println(client);  }

执行程序,控制台输出:

HardCodedTarget(type=DemoClient, url=http://localhost:8080)

但是,这样使用其实是有一定弊端的,且有一定使用门槛:

  1. 使用Builder构建时,使用者还需知道Feign#newInstance这个API
  2. 使用者必须知道Target的实现类HardCodedTarget才能完成构建

那么,作为一个合格的Builder,能否能屏蔽一些底层API,做到一步到位呢? 答案是肯定的,builder还额外提供了如下两个方法:

Feign.Builder    	// 很显然:方法一对调用者更加友好,因为Class和URL这些是使用者没有门槛的  	// 方法一依赖于方法二,它内部帮你构建Target的实例      public <T> T target(Class<T> apiType, String url) {        return target(new HardCodedTarget<T>(apiType, url));      }      public <T> T target(Target<T> target) {        return build().newInstance(target);      }

那么对于上例,其实完全可以非常简单的一步到位,简单都如下代码所示:

@Test  public void fun2(){      DemoClient client = Feign.builder().target(DemoClient.class, "http://localhost:8080");      System.out.println(client);  }

运行程序,结果同上。 因此这么做才是在生产上的推荐做法,同时也不得不夸赞一下:Feign.Builder是个合格的构建者。


总结

关于feign.Feign对象本身的介绍到这就结束了。 Feign的设计有很多自由的特色:高内聚性、工厂方法…当然这些都是源码设计上的特点。而在使用层面,我认为它是一个上手非常容易,但精通略难的一种技术,难一般体现在如下几点:

  1. 知识体系庞大,需要了解的甚多
  2. 设计不合理,阅读难、扩展难

作为一个流行的开源框架,原因显然不会是后者。

本文作为Feign核心内容的最后一篇文章就全部介绍完了,有了这些知识相信你完全有自信说自己已经精通Feign了。虽然还没有了解它如何友好支持JSON,如何和Spring、Spring Cloud整合使用,但相站在精通核心内容的基础上再看看它们,那些都是小儿科。但是,路漫漫其修远兮,任重而道远!!!

声明

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