Feign 系列(04)Contract 源码解析

Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/11563952.html#feign)

在 上一篇 文章中我们大致分析了一下 Feign 的工作原理,那 Feign 到底是如何适配 Feign、JAX-RS 1/2 的 REST 声明式注解,将方法的参数解析为 Http 的请求行、请求头、请求体呢?这里就不得不提 Contract 这个接口。

1. Feign 参数编码整体流程

图1:Feign 参数编码整体流程

sequenceDiagram
participant Client
Contract ->> MethodMetadata: 1. 解析方法元信息:parseAndValidatateMetadata(Class<?> targetType)
MethodMetadata ->> RequestTemplate.Factory: 2. 封装 MethodMetadata:buildTemplate
RequestTemplate.Factory ->> RequestTemplate: 3. 解析方法参数:create(argv)
RequestTemplate.Factory ->> Request: 4. request
Client ->> Request: 5. 发送Http请求:execute(Request request, Options options)

总结: 前两步是 Feign 代理生成阶段,解析方法参数及注解元信息。后三步是调用阶段,将方法参数编码成 Http 请求的数据格式。

public interface Contract {
List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType);
}

总结: Contract 接口将 UserService 中每个接口中的方法及其注解解析为 MethodMetadata,然后使用 RequestTemplate#request 编码为一个 Request。

public final class RequestTemplate implements Serializable {
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());
}
}

总结: RequestTemplate#request 编码为一个 Request 后就可以调用 Client#execute 发送 Http 请求。

public interface Client {
Response execute(Request request, Options options) throws IOException;
}

总结: Client 的具体实现有 HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty 等。本文关注前三步:即 Feign 方法元信息解析及参数编码过程。

2. Contract 方法注解及元信息解析

以 Feign 默认的 Contract.Default 为例:

首先回顾一下 Feign 注解的使用(@RequestLine @Headers @Body @Param @HeaderMap @QueryMap):

@Headers("Content-Type: application/json")
interface UserService {
@RequestLine("POST /user")
@Headers("Content-Type: application/json")
@Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
void user(@Param("user_name") String name, @Param("password") String password,
@QueryMap Map<String, Object> queryMap,
@HeaderMap Map<String, Object> headerMap, User user);
}
图2:Contract 方法元信息解析

sequenceDiagram
Contract ->> Method: 1. processAnnotationOnClass
Contract ->> Method: 2. processAnnotationOnMethod
Contract ->> Method: 3. processAnnotationsOnParameter
Note right of Method: 解析时也会校验合法性<br/>

总结: Contract.BaseContract#parseAndValidatateMetadata 会遍历解析 UserService 中的每个方法,按接口类上、方法上、参数上的注解,将其解析成 MethodMetadata。

 protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
MethodMetadata data = new MethodMetadata();
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
data.configKey(Feign.configKey(targetType, method)); // 1. 解析类上的注解
if (targetType.getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
processAnnotationOnClass(data, targetType); // 2. 解析方法上的注解
for (Annotation methodAnnotation : method.getAnnotations()) {
processAnnotationOnMethod(data, methodAnnotation, method);
}
Class<?>[] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes(); Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
for (int i = 0; i < count; i++) {
// isHttpAnnotation 表示参数上是否有注解存在
boolean isHttpAnnotation = false;
if (parameterAnnotations[i] != null) {
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
// 方法参数上不存在注解
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
} else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class) {
// 已经设置过 @FormParam JAX-RS规范
checkState(data.formParams().isEmpty(),
"Body parameters cannot be used with form parameters.");
// 已经设置过 bodyIndex,如 user(User user1, Person person) ×
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
data.bodyIndex(i);
data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
}
} return data;
}

这个方法也很好理解,接下来看一下 @RequestLine @Headers @Body @Param @HeaderMap @QueryMap 这些注解的具体解析过程。

2.1 processAnnotationOnClass

@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> targetType) {
if (targetType.isAnnotationPresent(Headers.class)) {
String[] headersOnType = targetType.getAnnotation(Headers.class).value();
checkState(headersOnType.length > 0, "Headers annotation was empty on type %s.",
targetType.getName());
Map<String, Collection<String>> headers = toMap(headersOnType);
headers.putAll(data.template().headers());
data.template().headers(null); // to clear
data.template().headers(headers);
}
}

总结: 类上只有一个注解:

  1. @Headers -> data.template().headers

2.2 processAnnotationOnMethod

protected void processAnnotationOnMethod(
MethodMetadata data, Annotation methodAnnotation, Method method) {
Class<? extends Annotation> annotationType = methodAnnotation.annotationType();
if (annotationType == RequestLine.class) {
String requestLine = RequestLine.class.cast(methodAnnotation).value();
checkState(emptyToNull(requestLine) != null,
"RequestLine annotation was empty on method %s.", method.getName()); Matcher requestLineMatcher = REQUEST_LINE_PATTERN.matcher(requestLine);
if (!requestLineMatcher.find()) {
throw new IllegalStateException(String.format(
"RequestLine annotation didn't start with an HTTP verb on method %s",
method.getName()));
} else {
data.template().method(HttpMethod.valueOf(requestLineMatcher.group(1)));
data.template().uri(requestLineMatcher.group(2));
}
data.template().decodeSlash(RequestLine.class.cast(methodAnnotation).decodeSlash());
data.template()
.collectionFormat(RequestLine.class.cast(methodAnnotation).collectionFormat()); } else if (annotationType == Body.class) {
String body = Body.class.cast(methodAnnotation).value();
checkState(emptyToNull(body) != null, "Body annotation was empty on method %s.",
method.getName());
if (body.indexOf('{') == -1) {
data.template().body(body);
} else {
data.template().bodyTemplate(body);
}
} else if (annotationType == Headers.class) {
String[] headersOnMethod = Headers.class.cast(methodAnnotation).value();
checkState(headersOnMethod.length > 0, "Headers annotation was empty on method %s.",
method.getName());
data.template().headers(toMap(headersOnMethod));
}
}

总结: 方法上可能有三个注解:

  1. @RequestLine -> data.template().method + data.template().uri
  2. @Body -> data.template().body
  3. @Headers -> data.template().headers

2.3 processAnnotationsOnParameter

protected boolean processAnnotationsOnParameter(
MethodMetadata data, Annotation[] annotations,int paramIndex) {
boolean isHttpAnnotation = false;
for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType == Param.class) {
Param paramAnnotation = (Param) annotation;
String name = paramAnnotation.value();
checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
paramIndex);
nameParam(data, name, paramIndex);
Class<? extends Param.Expander> expander = paramAnnotation.expander();
if (expander != Param.ToStringExpander.class) {
data.indexToExpanderClass().put(paramIndex, expander);
}
data.indexToEncoded().put(paramIndex, paramAnnotation.encoded());
isHttpAnnotation = true;
// 即不是@Headers和@Body上的参数,只能是formParams了
if (!data.template().hasRequestVariable(name)) {
data.formParams().add(name);
}
} else if (annotationType == QueryMap.class) {
checkState(data.queryMapIndex() == null,
"QueryMap annotation was present on multiple parameters.");
data.queryMapIndex(paramIndex);
data.queryMapEncoded(QueryMap.class.cast(annotation).encoded());
isHttpAnnotation = true;
} else if (annotationType == HeaderMap.class) {
checkState(data.headerMapIndex() == null,
"HeaderMap annotation was present on multiple parameters.");
data.headerMapIndex(paramIndex);
isHttpAnnotation = true;
}
}
return isHttpAnnotation;
}

总结: 参数上可能有三个注解:

  1. @Param-> data.indexToName

  2. @QueryMap-> data.queryMapIndex

  3. @HeaderMap-> data.headerMapIndex

    表1:Feign 注解解析对应值

    Feign 注解 MethodMetadata 中解析值
    @Headers data.template().headers
    @RequestLine data.template().method + data.template().uri
    @Body data.template().body
    @Param data.indexToName
    @QueryMap data.queryMapIndex
    @HeaderMap data.headerMapIndex

2.4 MethodMetadata

好了,上面讲解了半天,都是为了解析方法的元信息,目的就是为了屏蔽 Feign、JAX-RS 1/2、Spring Web MVC 等 REST 声明式注解的差异,那 MethodMetadata 到底有那些信息呢?

private String configKey;			// 方法签名,类全限名+方法全限名
private transient Type returnType; // 方法返回值类型
private Integer urlIndex; // 方法参数为url时,为 urlIndex
private Integer bodyIndex; // 方法参数没有任务注解,默认为 bodyIndex
private Integer headerMapIndex; // @HeaderMap
private Integer queryMapIndex; // @QueryMap
private boolean queryMapEncoded;
private transient Type bodyType;
private RequestTemplate template = new RequestTemplate(); // 核心
private List<String> formParams = new ArrayList<String>();
private Map<Integer, Collection<String>> indexToName =
new LinkedHashMap<Integer, Collection<String>>();
private Map<Integer, Class<? extends Expander>> indexToExpanderClass =
new LinkedHashMap<Integer, Class<? extends Expander>>();
private Map<Integer, Boolean> indexToEncoded = new LinkedHashMap<Integer, Boolean>();
private transient Map<Integer, Expander> indexToExpander;

总结: 到目前为至,Method 的方法的参数已经解析成 MethodMetadata,当方法调用时,会根据 MethodMetadata 的元信息将 argv 解析成 Request。

3. 参数解析成 Request

以 BuildTemplateByResolvingArgs 为例。

public RequestTemplate create(Object[] argv) {
RequestTemplate mutable = RequestTemplate.from(metadata.template());
// 1. 解析url参数
if (metadata.urlIndex() != null) {
int urlIndex = metadata.urlIndex();
checkArgument(argv[urlIndex] != null,
"URI parameter %s was null", urlIndex);
mutable.target(String.valueOf(argv[urlIndex]));
}
// 2. 解析参数argv成对应的对象
Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
int i = entry.getKey();
Object value = argv[entry.getKey()];
if (value != null) { // Null values are skipped.
if (indexToExpander.containsKey(i)) {
value = expandElements(indexToExpander.get(i), value);
}
for (String name : entry.getValue()) {
varBuilder.put(name, value);
}
}
} // 3. @Body中的参数占位符
RequestTemplate template = resolve(argv, mutable, varBuilder);
// 4. @QueryMap
if (metadata.queryMapIndex() != null) {
// add query map parameters after initial resolve so that they take
// precedence over any predefined values
Object value = argv[metadata.queryMapIndex()];
Map<String, Object> queryMap = toQueryMap(value);
template = addQueryMapQueryParameters(queryMap, template);
} // 5. @HeaderMap
if (metadata.headerMapIndex() != null) {
template =
addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
} return template;
}

总结: 将方法的参数解析成 RequestTemplate 后就简单了,只需要调用 request 即可最终解析成 Request。可以看到 Request 包含了 Http 请求的全部信息。到此,Feign 的参数解析全部完成。

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());
}

4. 思考:Feign 如何兼容 JAX-RS 1/2、Spring Web MVC

想必大家已经猜到,只需要实现自己的 Contract,将对应的注解信息解析成 MethodMetadata,即可完成适配工作。

  1. jaxrs Feign 原生支持,感兴趣的可以看一下其实现:feign.jaxrs.JAXRSContract
  2. Spring Web MVC Spring Cloud OpenFeign 提供了支持

每天用心记录一点点。内容也许不重要,但习惯很重要!

Feign 系列(04)Contract 源码解析的更多相关文章

  1. Mybatis 系列10-结合源码解析mybatis 的执行流程

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  2. Mybatis 系列8-结合源码解析select、resultMap的用法

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  3. Mybatis 系列7-结合源码解析核心CRUD 配置及用法

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  4. Mybatis 系列6-结合源码解析节点配置:objectFactory、databaseIdProvider、plugins、mappers

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  5. Mybatis 系列5-结合源码解析TypeHandler

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  6. Mybatis 系列4-结合源码解析节点:typeAliases

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  7. Mybatis 系列3-结合源码解析properties节点和environments节点

    [Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...

  8. 死磕 java同步系列之CyclicBarrier源码解析——有图有真相

    问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...

  9. 死磕 java同步系列之Phaser源码解析

    问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...

随机推荐

  1. SQL之in和like的连用实现范围内的模糊查询

    我们知道in可以实现一个范围内的查询,like可以实现模糊查询, 如 select *where col like 123%但是我们如果有一列attri,如123,132,165... 我们想实现12 ...

  2. 政府网站综合防护系统(网防G01)

    政府网站综合防护系统,简称“网防G01”,是首款专门针对政府网站及服务器等关键信息基础资源进行综合防护的产品,由公安部第一研究所和计算机病毒防治技术国家工程实验室(北京)研发. 网防G01的架构 由服 ...

  3. C++——右值引用

    1.绑定到一个将要销毁的右值的引用——移动 2.左值表达式——对象的身份:右值表达式——对象的值 ; int& r=i; int&& rr=i;//×,左值不能绑定到右值引用 ...

  4. phhstrom 快捷键

    TODO(表示待办事件)注释 快捷键 Alt+6 Alt+6 可以查看添加了//TODO注释的代码片段 一般我们在开发过程中由于时间或者各方面的时间来不及完成的代码,往往会先将逻辑写出来,实现留待以后 ...

  5. win10 打开 sql sever配置管理器

    !找到解决办法啦!WIN10系统 此电脑->右击->管理弹出以上界面啊哈哈哈还有一些解决办法在这个贴吧里...http://tieba.baidu.com/p/3000709047

  6. JS 多个条件判断

    // 多个条件判断 // 对象序列(Object) 推荐使用这一种 var obj = {'CJ':'成交', 'WCJ':'未成交'}; if (key in obj) { // TODO } // ...

  7. linux 杂七杂八

    一."init"是内核启动的第一个用户空间程序(PID=1),也是所有用户态进程的"大总管":所有内核态进程的大总管是PID=2的[kthreadd]: 二.v ...

  8. python学习笔记:操作Excle

    import xlwt #写excel import xlrd #读excel import xlutils #修改excel 一.写操作 1.写Excel import xlwt #写excel,导 ...

  9. 14.Jmeter聚合报告各项含义

    Aggregate Report 是 JMeter 常用的一个 Listener,中文为“聚合报告” Label:每个 JMeter 的 element(例如 HTTP Request)都有一个 Na ...

  10. a number of 和the number of用法

    a number of 和the number of用法 1.   A number of + 複數名詞 + 複數動詞           =some/或a lot of + 複數名詞 + 複數動詞 ...