飞道的博客

[享学Feign] 七、请求模版对象RequestTemplate和标准请求对象feign.Request

2416人阅读  评论(0)

你现在没有决定的权利,但你有决定未来的权利

–> 返回专栏总目录 <–
代码下载地址:https://github.com/f641385712/feign-learning

前言

通过前几篇文章,能够发现有个对象我们会频繁打交道,它就是Feign的请求模版对象RequestTemplate

feign.template.Template以及它的4个子模版都已经了解过了,体会到了模版设计的高扩展性和高弹性。而此处的RequestTemplate稍有不同,可以理解它是一个聚合,聚合有多种模版、参数、值从而提供转向标准请求对象feign.Request的能力。


正文

我们知道一个简单的实例方法RequestTemplate#request()就完成了模版对标准请求对象的转换,那么在它之前、之中、之后都做了什么呢?


RequestTemplate

它是HTTP目标的请求生成器,最终会被转换为feign.Request,所以也可以理解为它是feign.Request的模版配置对象。

RequestTemplate提供了非常多的配置选项,并且提供了组装、解析的能力,为最终转换为feign.Request提供支持。
可以把该类理解为UriTemplate的一个增强版,处理URI外,还有Headers、Body等更多的信息来支持一个标准的Http请求。


源码解析

@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
public final class RequestTemplate implements Serializable {

	// 查询参数:key是name,QueryTemplate表示遵循rfc6570规范的模版
	private final Map<String, QueryTemplate> queries = new LinkedHashMap<>();
	private final Map<String, HeaderTemplate> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
	private UriTemplate uriTemplate;

	// 注意:此处target表示的是最终准备请求的URL,并非feign.Target
	private String target;
	// 拼接在url后面的片段,一般表示锚点。如#bootmark
	private String fragment;
	
	// mark the new template resolved
	private boolean resolved = false;

	private HttpMethod method;
	// 请注意:这里使用的更加通用的UTF-8编码
	private transient Charset charset = Util.UTF_8;
	// 请求Body体 内包含bodyTemplate字符串
	private Request.Body body = Request.Body.empty();
	
	// 这两个属性就不解释了
	private boolean decodeSlash = true;
	private CollectionFormat collectionFormat = CollectionFormat.EXPLODED;

}

通过这些属性能够发现,它代表着一个目标方法的所有元数据信息,是组装一个标准Http请求的必备信息。

RequestTemplate:

  // 构造器:空构造是它唯一非@Deprecated的public构造器,但一般不用它来构建实例
  // 此构造器唯一调用处:`MethodMetadata`
  public RequestTemplate() {
    super();
  }

  // 将自己转换为标准的Request对象
  // 转换之前:请确保所有的template模版都已经被解析过了  resolved = true
  public Request request() {
    if (!this.resolved) {
      throw new IllegalStateException("template has not been resolved.");
    }
    return Request.create(this.method, this.url(), this.headers(), this.requestBody());
  }

  // 给请求模版指定Http方法
  public RequestTemplate method(HttpMethod method) {
    checkNotNull(method, "method");
    this.method = method;
    return this;
  }

  // 这个方法最特别的地方在于:给uriTemplate进行了重新赋值->把编码后的结果赋值给它
  public RequestTemplate decodeSlash(boolean decodeSlash) {
    this.decodeSlash = decodeSlash;
    this.uriTemplate =
        UriTemplate.create(this.uriTemplate.toString(), !this.decodeSlash, this.charset);
    return this;
  }


  // 该方法作用:给request设置一个uri
  // 这里会解析UriTemplate,并且拼接好所有的部分(包括查询参数、锚点等,但domain此处不管哦~)
  public RequestTemplate uri(String uri) { ... }
  public RequestTemplate uri(String uri, boolean append) { ... }
  public String url() { ... }
  public String path() { ... }

  // 它是组装所有的所有的所有的部分,拼接成一个完成的URL
  // 形如:http://www.baidu.com/aaa/bbb?name=YourBatman#mark
  public RequestTemplate target(String target) { ... }
	
  // 获取所有template的所有参数名称们,也就是key们	
  public List<String> variables() { ... }

  // Body Template to resolve.
  public String bodyTemplate() {
    return body.bodyTemplate();
  }
  // 查询参数
  public String queryLine() { ... }

这是RequestTemplate提供的一些基本访问方法,相对简单。接下来讲述的将是它的重难点:根据参数resolve()处理模版

RequestTemplate:

  // 使用提供的变量值替换解析所有表达式。
  // 注意:变量值是一份,所有的模版都会解析哦
  public RequestTemplate resolve(Map<String, ?> variables) {
		StringBuilder uri = new StringBuilder();
		
		// 特别注意:在基础上赋值一份出来,所以操作的是这份新的,return出去的也是新的哦
		RequestTemplate resolved = RequestTemplate.from(this);

		... //1、解析uriTemplate,·uriTemplate.expand(variables)·
	
		// 2、解析查询参数模版,这块相对复杂。用&连接起来
		if (!this.queries.isEmpty()) {
		
			// 先处理模版:一个模版一个模版的处理
			Iterator<QueryTemplate> queryTemplates = this.queries.values().iterator();
			while (queryTemplates.hasNext()) {
				...
				String queryExpanded = queryTemplate.expand(variables);
				...
				query.append("&"); 
			}
			...
			// 设值查询参数
			resolved.uri(uri.toString());

			//再处理请求头模版
			if (!this.headers.isEmpty()) {
				for (HeaderTemplate headerTemplate : this.headers.values()) {
					...
					String headerValues = header.substring(header.indexOf(" ") + 1);
					...
				}
			}

			// 处理body模版
			// 说明:body里持有BodyTemplate的引用,所以底层依旧是bodyTemplate.expand(variables)
			resolved.body(this.body.expand(variables));
			
			// 标志位:标记为已处理
			resolved.resolved = true;
			return resolved;
		}
		
  }

RequestTemplate#resolve()是其最重要的一个方法:用于填充四大模版TemplateUriTemplate、QueryTemplate、HeaderTemplate、BodyTemplate在转换为feign.Request之前,肯定会使用resolve()此方法完成数据填充


RequestTemplate.Factory

Feign的设计上有个特点:几乎所有的实例都不采用直接new的方式,而是使用工厂方法来创建。RequestTemplate.Factory顾名思义,它就是用来构建/创建一个RequestTemplate实例的工厂接口。

interface Factory {
   RequestTemplate create(Object[] argv);
}

接口示意很简单:根据参数数组argv构建出一个RequestTemplate实例。它有如下实现类:

注意:这些实现类均写在ReflectiveFeign里面,并且均为private static的,所以均为内部实现,是一种高内聚的表现。
该接口主要作用是根据最原始的数据:方法参数、MethodMetadata元数据等,完成到RequestTemplate实例的封装,掌握了它关于数据编码的逻辑将拨开云雾见青天。


BuildTemplateByResolvingArgs

它并不是一个抽象类,作用是根据args、MethodMetadataQueryMapEncoder构建出一个请求模版对象。

private static class BuildTemplateByResolvingArgs implements RequestTemplate.Factory {

	private final QueryMapEncoder queryMapEncoder;
	protected final MethodMetadata metadata;
	
	// 这里的Expander指的是feign.Param.Expander,也就是由注解决定使用哪种Expander,默认是ToStringExpander
	// 这里的key 数字类型:表示参数的index...
	private final Map<Integer, Expander> indexToExpander = new LinkedHashMap<Integer, Expander>();

	// 唯一构造器:给前两个属性赋值。并且并且并且根据这两个属性
	// 计算出indexToExpander的值,然后存储着
	private BuildTemplateByResolvingArgs(MethodMetadata metadata, QueryMapEncoder queryMapEncoder) {
      this.metadata = metadata;
      this.queryMapEncoder = queryMapEncoder;

	  // 计算
	  ...
	  for (Entry<Integer, Class<? extends Expander>> indexToExpanderClass : metadata.indexToExpanderClass().entrySet()) {
	  		...
	  		indexToExpander.put(indexToExpanderClass.getKey(), indexToExpanderClass.getValue().newInstance());
	  		...
	  }
	}
}

通过构造器收集到了每个方法参数应该使用的Expander类型,这是在“启动阶段”,也就是在构建阶段完成的

接下来自然而然就是最重要的接口方法的实现:

BuildTemplateByResolvingArgs:

    @Override
    public RequestTemplate create(Object[] argv) {
    	// 同样的,先拷贝一份出来RequestTemplate出来,但这并不是最终return的
		RequestTemplate mutable = RequestTemplate.from(metadata.template());

		// varBuilder装载所有的k-v
		Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
		for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
			int i = entry.getKey();
			Object value = argv[i];
			if (value != null) {
				...
				for (String name : entry.getValue()) {
					varBuilder.put(name, value);
				}
			}
		}

		// 调用RequestTemplate#resolve()方法,得到一个全新的实例
		RequestTemplate template = resolve(argv, mutable, varBuilder);
		// 支持@QueryMap
		if (metadata.queryMapIndex() != null) {
			...
		}
		// 支持@HeaderMap
		if (metadata.headerMapIndex() != null) {
			...
		}
		return template;
    }

	// 本类默认实现,直接嗲用RequestTemplate填充模版
	// 两个子类对此方法均由复写...
    protected RequestTemplate resolve(Object[] argv,
                                      RequestTemplate mutable,
                                      Map<String, Object> variables) {
      return mutable.resolve(variables);
    }

可以看到,这里解析了4大Template,共用一份数值对象variables

本创建方法的作用描述:解析元数据对象MethodMetadata,把对应参数填进RequestTemplate各个属性里,最终依赖于RequestTemplate#resolve()方法完成解析。

说明:MethodMetadata我们知道它的数据均来自于对注解的解析,这个注解可以是源生注解,也可以是你自定义的注解信息(比如Spring MVC的扩展)


BuildEncodedTemplateFromArgs

它在父类的基础上,加入了解码器feign.codec.Encoder,因此对于方法体Body的参数,它可以先让解码器解码后(放进body里)再执行父类的解析逻辑。

private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {

    private final Encoder encoder;
    ... // 省略构造器

	// 复写父类的resolve()解析方法,让解码器参与进来
    @Override
    protected RequestTemplate resolve(Object[] argv,
                                      RequestTemplate mutable,
                                      Map<String, Object> variables) {
      Object body = argv[metadata.bodyIndex()];
      checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
      	...
		encoder.encode(body, metadata.bodyType(), mutable);
		...
	}

}

它要求bodyIndex != null才会生效,对应的encoder什么时候生效的逻辑之前也说过,此处可重复一次:没有标注@Param注解时候,才会交给编码器编码进body体里


BuildFormEncodedTemplateFromArgs

作用几乎同上,也是利用Encoder进行了编码,不过它会把Map<String, Object> variables参数值们以from表单的形式提交,这便它的区别。

关于元数据什么时候提取为Body参数,什么时候当作Form表单参数,请参考feign.Contract详解。


feign.Request

这是Feign的标准的、不可变的Http请求实体。

public final class Request {

  // 这些参数很容易理解:它就是Http请求的各个部分
  private final HttpMethod httpMethod;
  private final String url;
  private final Map<String, Collection<String>> headers;
  private final Body body;
  ... // 省略构造器。构造器是私有的,并不能直接构造实例对象,而是提供了静态方法

	// 获取编码 -> 来自于body
  public Charset charset() {
    return body.encoding;
  }
  // body的二进制表示形式 --> 通用
  public byte[] body() {
    return body.data;
  }
  ... // 省略其它get方法(并没有set方法哦~~~)
}

从属性中能看出它是一个标准的Http请求对象,那么如何构建它呢?这就需要下面这些静态工厂方法:

Request:

  // 特点:method使用字符串,body使用二进制 + 编码的方式
  // Body.encoded(body, charset)可以把二进制 + 编码 转换为一个Body对象
  // 所以这个构造器是面向用户良好的创建方法,因此使用也较多
  public static Request create(String method,
                               String url,
                               Map<String, Collection<String>> headers,
                               byte[] body,
                               Charset charset) {
    checkNotNull(method, "httpMethod of %s", method);
    HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
    return create(httpMethod, url, headers, body, charset);
  }
  ...
  // 最终调用都是Request唯一的构造器:new Request(httpMethod, url, headers, body);

	// Http标准方法的枚举
  public enum HttpMethod {
    GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH
  }
  // 发送请求的选项:链接超时(默认10s),读取超时(默认60秒)
  public static class Options {
    private final int connectTimeoutMillis;
    private final int readTimeoutMillis;
    private final boolean followRedirects;
    ...
  }

Request对象通过静态工厂方法创建,并且不可变
在这其中,最有文章可循的当属Body对象:


Body

顾名思义,它代表Http的请求体,它也是Request的一个内部类,不过是public的,允许外部完成构建。

public static class Body {

	// 数据用字节数组表述,可以表示任意类型,最为通用
    private final byte[] data;
    private final Charset encoding;

	// 但是请注意:body内容可能来自于模版,这是需要区分的一点
	// 很显然,bodyTemplate是可以为null的
    private final BodyTemplate bodyTemplate;

	... // 省略私有构造器

	// 根据variables,经过BodyTemplate产生一个新的Body对象
	// 这是根据模版构建Body的一种方式:使用较多
    public Request.Body expand(Map<String, ?> variables) {
      if (bodyTemplate == null)
        return this;
      return encoded(bodyTemplate.expand(variables).getBytes(encoding), encoding);
    }
    // 直接传值构建:静态方法
    public static Request.Body encoded(byte[] bodyData, Charset encoding) {
      return new Request.Body(bodyData, encoding, null);
    }
    // 静态方法:空消息体  data为null 编码类型为null...
    public static Body empty() {
      return new Request.Body(null, null, null);
    }

	...
	// 获取body的二进制表示形式。注意:方法名不叫getBytes
    public byte[] asBytes() {
      return data;
    }
    // 转为String的前提条件:data和encoding都不能为null
    public String asString() {
      return (encoding != null && data != null) ? new String(data, encoding) : "Binary data";
    }

    public int length() {
      return data != null ? data.length : 0;
    }
    // 拿到Body的模版(可能为null)
    public String bodyTemplate() {
      return (bodyTemplate != null) ? bodyTemplate.toString() : null;
    }
}

它最大的特点是把BodyTemplate放在了本处,而并没有放在RequestTemplate里,RequestTemplate选择放了Body而非BodyTemplate,这点需要引起注意。


总结

本文介绍了Feign请求相关的最重要两个对象:RequestTemplateRequest。前者负责各种属性值的收集、模版的解析等逻辑,相对复杂;后者可以理解为一个不可更改的POJO,相对简单。

声明

原创不易,码字不易,多谢你的点赞、收藏、关注。把本文分享到你的朋友圈是被允许的,但拒绝抄袭。你也可【左边扫码/或加wx:fsx641385712】邀请你加入我的 Java高工、架构师 系列群大家庭学习和交流。


转载:https://blog.csdn.net/f641385712/article/details/104291545
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场