Feign 系列(04)Contract 源码解析

Feign 系列(04)Contract 源码解析

[TOC]

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 提供了支持


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

原文地址:https://www.cnblogs.com/binarylei/p/11576148.html

时间: 2024-10-10 07:47:06

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

死磕 java同步系列之ReentrantReadWriteLock源码解析

问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的TreeMap? 简介 读写锁是一种特殊的锁,它把对共享资源的访问分为读访问和写访问,多个线程可以同时对共享资源进行读访问,但是同一时间只能有一个线程对共享资源进行写访问,使用读写锁可以极大地提高并发量. 特性 读写锁具有以下特性: 是否互斥 读 写 读 否 是 写 是 是 可以看到,读写锁除了读读

死磕 java同步系列之Semaphore源码解析

问题 (1)Semaphore是什么? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什么场景中? (4)Semaphore的许可次数是否可以动态增减? (5)Semaphore如何实现限流? 简介 Semaphore,信号量,它保存了一系列的许可(permits),每次调用acquire()都将消耗一个许可,每次调用release()都将归还一个许可. 特性 Semaphore通常用于限制同一时间对共享资源的访问次数上,也就是常说的限流. 下面我们一起来学习Java

死磕 java同步系列之CountDownLatch源码解析

??欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)CountDownLatch是什么? (2)CountDownLatch具有哪些特性? (3)CountDownLatch通常运用在什么场景中? (4)CountDownLatch的初始次数是否可以调整? 简介 CountDownLatch,可以翻译为倒计时器,但是似乎不太准确,它的含义是允许一个或多个线程等待其它线程的操作执行完毕后再执行后续的操作. Cou

死磕 java同步系列之StampedLock源码解析

问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWriteLock的对比? 简介 StampedLock是java8中新增的类,它是一个更加高效的读写锁的实现,而且它不是基于AQS来实现的,它的内部自成一片逻辑,让我们一起来学习吧. StampedLock具有三种模式:写模式.读模式.乐观读模式. ReentrantReadWriteLock中的读和写都是

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

问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier,回环栅栏,它会阻塞一组线程直到这些线程同时达到某个条件才继续执行.它与CountDownLatch很类似,但又不同,CountDownLatch需要调用countDown()方法触发事件,而CyclicBarrier不需要,它就像一个栅栏一样,当一组线程都到达了栅栏处才继续往下走. 使用方法 pu

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

问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这样一种场景,一个大任务可以分为多个阶段完成,且每个阶段的任务可以多个线程并发执行,但是必须上一个阶段的任务都完成了才可以执行下一个阶段的任务. 这种场景虽然使用CyclicBarrier或者CountryDownLatch也可以实现,但是要复杂的多.首先,具体需要多少个阶段是可能会变的,其次,每个阶

Redux系列x:源码解析

写在前面 redux的源码很简洁,除了applyMiddleware比较绕难以理解外,大部分还是 这里假设读者对redux有一定了解,就不科普redux的概念和API啥的啦,这部分建议直接看官方文档. 此外,源码解析的中文批注版已上传至github,可点击查看.本文相关示例代码,可点击查看. 源码解析概览 将redux下载下来,然后看下他的目录结构. npm install redux 这里我们需要关心的主要是src目录,源码解析需要关心的文件都在这里面了 index.js:redux主文件,主

Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例

java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例 概要  和学习ArrayList一样,接下来呢,我们先对LinkedList有个整体认识,然后再学习它的源码:最后再通过实例来学会使用LinkedList.内容包括:第1部分 LinkedList介绍第2部分 LinkedList数

Feign 系列(05)Spring Cloud OpenFeign 源码解析

Feign 系列(05)Spring Cloud OpenFeign 源码解析 [TOC] Spring Cloud 系列目录(https://www.cnblogs.com/binarylei/p/11563952.html#feign) 在 上一篇 文章中我们分析 Feign 参数解析的整个流程,Feign 原生已经支持 Feign.JAX-RS 1/2 声明式规范,本文着重关注 Spring Cloud 是如果整合 OpenFeign 的,使之支持 Spring MVC? 1. Sprin