zoukankan      html  css  js  c++  java
  • Feign 系列(04)Contract 源码解析

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

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

  • 相关阅读:
    后期生成事件命令copy /y
    SevenZipShaper压缩类
    vs2017
    WCF路由服务
    微服务--
    各种流程图的绘画网路工具 processon
    ROC 准确率,召回率 F-measure理解(转载)
    Unix OpenCV安装
    转载:tar 解压缩命令~
    cppreference经验总结
  • 原文地址:https://www.cnblogs.com/binarylei/p/11576148.html
Copyright © 2011-2022 走看看