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

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

–> 返回专栏总目录 <–
代码下载地址: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高工、架构师 系列群大家庭学习和交流。
往期精选

发布了377 篇原创文章 · 获赞 572 · 访问量 51万+
展开阅读全文

openfeign里面配置httpclient出错。

08-18

因为现在需要使用get方式传递对象参数,所以尝试在openfeign里面配置httpclient。 配置代码如下: 在yml文件里面增加了配置信息 ``` feign: httpclient: enabled: true ``` 在pom.xml文件中引入的依赖: ``` <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.3</version> </dependency> <!-- 使用Apache HttpClient替换Feign原生httpclient --> <dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign-httpclient</artifactId> <version>8.16.1</version> </dependency> ``` 配置好以后尝试使用get方式传递对象参数。 服务提供端代码: ``` @GetMapping(value = "testAddRole1",consumes = "application/json") public JsonResult addTest1(@RequestBody Role role){ roleService.addRole(role); return JsonResult.getInstant(ReturnCodeEnum.SUCCESS); } ``` 服务调用接口为: ``` @GetMapping(value = "/testAddRole1",consumes = "application/json") JsonResult testAddRole1(@RequestBody Role role); ``` 现在在尝试调用接口后发现服务提供方可以接受到参数并且能够写到数据库中,但是在服务调用方拿不到正确的返回值并且会报错,错误如下: ``` Caused by: java.lang.NoSuchMethodError: feign.Response.create(ILjava/lang/String;Ljava/util/Map;Lfeign/Response$Body;)Lfeign/Response; ``` 查看源码得知,openfeign在接受返回值时调用的不是httpclient的feign-core包的代码而是调用的本身的feign-core的代码,而本身的feign-core包中的Response类没有create方法。 得知原因后我将httpclient的依赖在pom文件中的位置上移,使openfeign优先调用httpclient的feign-core包的代码,结果在启动项目时就报错,报错信息为: ``` org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'test1Controller' defined in file [D:\workspace\basic\openfeign\target\classes\cn\cloudscope\openfeignTest\Test1\controller\Test1Controller.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cn.cloudscope.openfeignTest.Test1.service.Test1Service': FactoryBean threw exception on object creation; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'feignRetryer' defined in org.springframework.cloud.openfeign.FeignClientsConfiguration: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [feign.Retryer]: Factory method 'feignRetryer' threw exception; nested exception is java.lang.NoSuchFieldError: NEVER_RETRY ``` 查看原因得知是两个feign-core包中的Retryer接口不一致导致的,求来个大神帮忙解决一下,或者怎么调整jar包的版本去解决这个问题。openfeign的feign-core版本为10.1.0 httpclient的版本为8.16.1 问答

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie

分享到微信朋友圈

×

扫一扫,手机浏览