前言
紧接上文,之前分析了方法处理器是如何进行加载及执行的,其中提到了,在调用Feign 接口方法时,会根据方法参数创建请求模板(RequestTemplate
),接下来我们分析下这个是如何加载及执行的。
核心类
RequestTemplate
Feign 中存在各种各样的模板类,RequestTemplate
就是其中封装了执行请求需要的相关信息,比如请求方式、路径等。
RequestTemplate
中封装了执行请求所需的相关信息:
// 查询模板
private final Map<String, QueryTemplate> queries = new LinkedHashMap();
// 消息头模板
private final Map<String, HeaderTemplate> headers;
// 请求的目标地址
private String target;
// URL 后面的片段
private String fragment;
// 是否已解析
private boolean resolved;
// URI 模板
private UriTemplate uriTemplate;
// 请求体模板
private BodyTemplate bodyTemplate;
// 请求方式
private HttpMethod method;
// 字符集
private transient Charset charset;
// 请求体
private Body body;
// 是否斜杠转义
private boolean decodeSlash;
// 集合格式化
private CollectionFormat collectionFormat;
// 方法元数据
private MethodMetadata methodMetadata;
// 代理的目标对象
private Target<?> feignTarget;
RequestTemplate
提供了一个核心方法,用于解析数据,创建模板。
Feign 还有其他四大模板对象UriTemplate
、QueryTemplate
、HeaderTemplate
、BodyTemplate
,都封装在了RequestTemplate
中,分别对应了请求路径、查询参数、消息头、请求体模板。
源码分析
1. 进入工厂类
方法执行器执行时,首先就会创建请求模板:
调用的是RequestTemplate
的内部工厂接口的实现类BuildTemplateByResolvingArgs
(通过解析方法参数构建模板工厂类),该方法会根据方法元数据创建RequestTemplate
// argv 执行方法的参数
public RequestTemplate create(Object[] argv) {
// 使用方法元数据创建RequestTemplate
RequestTemplate mutable = RequestTemplate.from(this.metadata.template());
// 设置目标对象
mutable.feignTarget(this.target);
// 方法元数据是否包含了URL 也就是参数中是否有URL 对象
if (this.metadata.urlIndex() != null) {
// 有URL 则设置请求的目标(地址)
int urlIndex = this.metadata.urlIndex();
Util.checkArgument(argv[urlIndex] != null, "URI parameter %s was null", new Object[]{
urlIndex});
mutable.target(String.valueOf(argv[urlIndex]));
}
// 封装请求参数KV 键值对 ,@RequestParams 注解时会用到
Map<String, Object> varBuilder = new LinkedHashMap();
Iterator var4 = this.metadata.indexToName().entrySet().iterator();
// 死循环
while(true) {
Entry entry;
int i;
Object value;
do {
// 元数据中有indexToName
if (!var4.hasNext()) {
// 调用RequestTemplate 的解析方法
RequestTemplate template = this.resolve(argv, mutable, varBuilder);
// 如果存在@SpringQueryMap (方法上参数上有@SpringQueryMap注解,可以传递对象参数)
// 将对象解析为Map, 在URL后面使用键值对拼接
if (this.metadata.queryMapIndex() != null) {
// 获取@SpringQueryMap标识的参数对象
Object value = argv[this.metadata.queryMapIndex()];
// 转对象为MAP
Map<String, Object> queryMap = this.toQueryMap(value);
// 添加请求参数
template = this.addQueryMapQueryParameters(queryMap, template);
}
// 存在@HeaderMap 就添加消息头
if (this.metadata.headerMapIndex() != null) {
template = this.addHeaderMapHeaders((Map)argv[this.metadata.headerMapIndex()], template);
}
return template;
}
// 有indexToName
entry = (Entry)var4.next();
i = (Integer)entry.getKey(); // 参数序号
value = argv[(Integer)entry.getKey()]; // 参数值
} while(value == null);
// 没有indexToName 继续执行
if (this.indexToExpander.containsKey(i)) {
// 参数值
value = this.expandElements((Expander)this.indexToExpander.get(i), value);
}
Iterator var8 = ((Collection)entry.getValue()).iterator();
// 循环,将参数名,参数类型解析为Map ,全部解析完后,还是会回到 this.resolve(argv, mutable, varBuilder)
while(var8.hasNext()) {
String name = (String)var8.next();
varBuilder.put(name, value);
}
}
}
2. 请求参数为对象【POST】
首先看下参数类型为对象时,请求模板时如何创建的。
Feign 接口:
@PostMapping("/order/post")
public Order post(Order order);
服务提供者:
@PostMapping("/post")
public Order insertOrder(@RequestBody Order order) throws InterruptedException {
return order;
}
可以看到请求方式为Post 、参数为对象时,在解析元数据的时候,会直接将其解析为请求体,
在创建模板时,会直接调用BuildEncodedTemplateFromArgs
的resolve
方法:
resolve
中会使用编码器,将实体类转为请求体,最后调用RequestTemplate
的解析方法处理参数。
protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) {
// 根据参数下标,获取到当前参数对象
Object body = argv[this.metadata.bodyIndex()];
// 检查参数,不能为null
Util.checkArgument(body != null, "Body parameter %s was null", new Object[]{
this.metadata.bodyIndex()});
try {
// 调用SpringEncoder,将参数对象进行编码,并设置到RequestTemplate 中
this.encoder.encode(body, this.metadata.bodyType(), mutable);
} catch (EncodeException var6) {
throw var6;
} catch (RuntimeException var7) {
throw new EncodeException(var7.getMessage(), var7);
}
// 调用RequestTemplate 的resolve
return super.resolve(argv, mutable, variables);
}
}
最后,这种请求方式的参数就被转化为了byte 数组封装在RequestTemplate
中了
3. 请求参数为对象【GET】
可以看到Get 请求时,也会将参数编码到请求体中。
这个时候底层HTTP 框架就会报错method GET must not have a request body.
,GET 请求不支持请求体。
这个时候就需要用到@SpringQueryMap
注解,spring cloud在2.1.x版本中提供了@SpringQueryMap
注解,可以传递对象参数,框架自动解析。
我们修改下服务调用者及提供者:
@GetMapping(value = "/order/post")
public Object post(@SpringQueryMap Order order);
@GetMapping("/post")
public Order insertOrder(Order order) throws InterruptedException {
return order;
}
首先可以看到,解析方法元数据的时候,会解析@SpringQueryMap
标识的参数,设置queryMapIndex
下标为0。
在解析模板的时候,因为queryMapIndex
下标为0,所以会进入到addQueryMapQueryParameters
方法,添加请求参数,
在addQueryMapQueryParameters
方法中,会循环键值对,并将其转化为QueryTemplate
,可以看到直接使用值的ToString方法进行值的获取。
private RequestTemplate addQueryMapQueryParameters(Map<String, Object> queryMap, RequestTemplate mutable) {
Entry currEntry;
ArrayList values;// 存放值集合
boolean encoded;
// 循环Map,将键值对添加到queryTemplate中
for(Iterator var3 = queryMap.entrySet().iterator(); var3.hasNext(); mutable.query(encoded ? (String)currEntry.getKey() : UriUtils.encode((String)currEntry.getKey()), values)) {
currEntry = (Entry)var3.next(); // 键值对
values = new ArrayList();
encoded = this.metadata.queryMapEncoded(); // QueryMap是否已编码
Object currValue = currEntry.getValue(); // 键对应的值
if (currValue instanceof Iterable) {
// 如果值为集合,调用ToString
Iterator iter = ((Iterable)currValue).iterator();
while(iter.hasNext()) {
Object nextObject = iter.next();
values.add(nextObject == null ? null : (encoded ? nextObject.toString() : UriUtils.encode(nextObject.toString())));
}
} else {
// 值为null 直接返回null, 不为null,则直接ToString
values.add(currValue == null ? null : (encoded ? currValue.toString() : UriUtils.encode(currValue.toString())));
}
}
return mutable;
}
最后,值不为null 的键值对,会被添加到QueryTemplate
中,在执行请求时会将这些查询参数解析出来,这些参数会拼接在URL后面。
4. @RequestParam 注解【GET】
在Spring MVC中,@RequestParam
将请求参数绑定到你控制器的方法参数上,那么使用了这个注解的模板是怎么生成的呢?
首先可以看到在元数据中,这些参数会被解析为MethodMetadata
的indexToExpander
和indexToName
属性。
所以在解析模板的时候会进入到以下代码中
最终将这些参数解析为键值对:
然后调用RequestTemplate
的resolve
方法,在该方法中,会将请求参数键值对解析为&
符号链接的字符串,并添加到最终发送请求的URI中。
// variables 为参数键值对
public RequestTemplate resolve(Map<String, ?> variables) {
// 请求路径
StringBuilder uri = new StringBuilder();
RequestTemplate resolved = from(this);
// 没有URL 则会创建一个空路径
if (this.uriTemplate == null) {
this.uriTemplate = UriTemplate.create("", !this.decodeSlash, this.charset);
}
// 请求路径: /order/insert
String expanded = this.uriTemplate.expand(variables);
if (expanded != null) {
uri.append(expanded); // 添加到变量uri 中
}
String headerValues;
String queryString;
if (!this.queries.isEmpty()) {
// 查询参数尾部空
resolved.queries(Collections.emptyMap());
StringBuilder query = new StringBuilder();
Iterator queryTemplates = this.queries.values().iterator();
// 拼接参数请求字符串
while(queryTemplates.hasNext()) {
QueryTemplate queryTemplate = (QueryTemplate)queryTemplates.next();
headerValues = queryTemplate.expand(variables);
if (Util.isNotBlank(headerValues)) {
query.append(headerValues);
if (queryTemplates.hasNext()) {
query.append("&");
}
}
}
// 创建请求后面拼接的参数字符串=》accountId=11&commodityCode=sss&count=22&money=33
queryString = query.toString();
if (!queryString.isEmpty()) {
Matcher queryMatcher = QUERY_STRING_PATTERN.matcher(uri);
if (queryMatcher.find()) {
uri.append("&");
} else {
uri.append("?");
}
uri.append(queryString);
}
}
// 最终的URI=》 /order/insert?accountId=11&commodityCode=sss&count=22&money=33
resolved.uri(uri.toString());
if (!this.headers.isEmpty()) {
resolved.headers(Collections.emptyMap());
Iterator var9 = this.headers.values().iterator();
while(var9.hasNext()) {
HeaderTemplate headerTemplate = (HeaderTemplate)var9.next();
queryString = headerTemplate.expand(variables);
if (!queryString.isEmpty()) {
headerValues = queryString.substring(queryString.indexOf(" ") + 1);
if (!headerValues.isEmpty()) {
resolved.header(headerTemplate.getName(), Literal.create(headerValues));
}
}
}
}
// 处理Body
if (this.bodyTemplate != null) {
resolved.body(this.bodyTemplate.expand(variables));
}
resolved.resolved = true;
return resolved;
}
最终这个请求模板内容如下:
5. 转换为Request
请求模板创建成功以后,会将其转换为Request
对象,这个步骤是在方法处理器中进行的。
转换的时候,会调用拦截器:
Request
对象的创建,还是调用的RequestTemplate
的request()
方法:
public Request request() {
if (!this.resolved) {
throw new IllegalStateException("template has not been resolved.");
} else {
return Request.create(this.method, this.url(), this.headers(), this.body, this);
}
}
创建过程中,会将请求用的到路径、请求方式、模板等,封装在Request
对象中。
最终,这个请求对象经过负载均衡器,获取到请求真实IP,传递给底层HTTP 框架,再由其转为自身的Request 对象,执行请求,整个流程就结束了。