Feign 系列(04)Contract 源码解析

Posted binarylei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 提供了支持

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

以上是关于Feign 系列(04)Contract 源码解析的主要内容,如果未能解决你的问题,请参考以下文章

SpringCloud系列之Feign-8.深入了解FeignContract协议解析过程

SpringCloud系列之Feign-5.@EnableFeignClients底层机制深度解析

深入浅出SpringCloud原理及实战「Netflix系列之Fegin」从源码层面让你认识Feign工作流程和运作机制

feign集成hystrix全流程源码分析

Feign源码解析二

SpringCloud系列之Feign-6.Feign上下文构建解析