Feign高级功能

Feign NB

Posted by hcy on December 17, 2019

Feign高级功能

动态修改请求地址

像这样,创建接口时放置一个类型为java.net.URI的参数,这样真正发送请求时就会以此uri为准。

1
2
3
4
    interface dyPath {
        @RequestLine("GET /get/item")
        String getItem(URI uri);
    }

动态请求配置Options

放置一个类型为Options的参数在接口中,这样发送请求时就会使用此类作为配置,否则会使用默认的配置

1
2
3
4
    interface dyPath {
        @RequestLine("GET /get/item")
        String getItem(Request.Options options);
    }
  • 源码在这里:
1
2
3
4
5
6
7
8
9
10
11
//feign.SynchronousMethodHandler#191  
Options findOptions(Object[] argv) {
    if (argv == null || argv.length == 0) {
      return this.options;
    }
    return Stream.of(argv)
        .filter(Options.class::isInstance)
        .map(Options.class::cast)
        .findFirst()
        .orElse(this.options);
  }

@QueryMap注解

  • 这个注解如果注在Map上,此Map的键只能是String类型的如Map<String,Object>,其他类型会报错。

    1
    2
    3
    4
    5
    
      //源码在这里: feign.Contract.BaseContract#checkMapKeys
          if (keyClass != null) {
              checkState(String.class.equals(keyClass),
                         "%s key must be a String: %s", name, keyClass.getSimpleName());
          }
    
  • 如果在@RequestLine的链接中放置了参数, @QueryMap中有同名参数是一个空集合,则会把前面的参数删掉,所以想要删除某参数,也可以直接设置一个空集合参数,同理添加heads也有这样的删除策略。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
          interface Fq {
              @RequestLine(value = "GET /cms/materials/create?username={username}")
              String getItemCoupon(@Param(value = "username") String username
                              , @QueryMap Map<String, Object> params);
          }
        
      //调用过程
           HashMap<String, Object> params = new HashMap<>();
           params.put("username", Collections.emptyList());
           String itemCoupon = target.getItemCoupon("小明", params);
        
      //参数Map里面有一‘username’和链接上的参数同名了,且是一个空集合,即使链接上的`username`有值,也会导致username参数被删除
        
      //实际访问地址 GET http://localhost/create HTTP/1.1
        
    
  • @QueryMap解析出来的参数只会拼接在url后面,不会当成请求体,无论请求类型是什么。

    有时候post请求不希望参数拼在链接后面,就不能用这个了。

@Param注解注意事项

注解标记的参数,如果没有在任何地方被使用,则会被放入MethodMetadataformParams中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
      super.registerParameterAnnotation(Param.class, (paramAnnotation, data, paramIndex) -> {
        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());
        //这里检测,没有任何地方使用它,则放入formParams中
        if (!data.template().hasRequestVariable(name)) {
          data.formParams().add(name);
        }
      });

如下面写法的两个参数nameage 就是未使用因为RequestLineHeaders没有用它:

1
2
3
4
5
    interface TestJson {
        @RequestLine(value = "POST /test")
        @Headers("Content-Type:application/json")
        Response getItemCoupon(@Param("name") String name,@Param("age") Integer age);
    }

在发送请求时,会将他们组成一个map然后传入encoder中进行编码,编码后的结果作为请求的body使用,而默认的encoder不能处理map类型的,会报错.

feign.codec.EncodeException: class java.util.LinkedHashMap is not a type supported by this encoder

添加一个JacksonEncoder后可以将他们序列化成JSON字符串作为请求body,即使是GET请求也会变成请求body,所以如果这不是你想要的结果,就不要添加了参数却不使用它。

如果不想将他们转成JSON,可以自己写encoder来处理 。

没有加任何注解的参数

默认情况下允许存在一个没有注解的参数(注意URI不包括在内),将它的参数索引设置成MeteDatebodyIndex,发送请求前,会把这个参数的值取出来,然后调用encoder进行编码,编码的结果当成请求body使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// feign.Contract.BaseContract#parseAndValidateMetadata()

//参数类型为接口的class类,需要解析的method
protected MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
      MethodMetadata data = new MethodMetadata();
      data.targetType(targetType);
      data.method(method);
      data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
      data.configKey(Feign.configKey(targetType, method));

      if (targetType.getInterfaces().length == 1) {
        processAnnotationOnClass(data, targetType.getInterfaces()[0]);
      }
    //解析类上的注解
      processAnnotationOnClass(data, targetType);
	//循环解析方法上的所有注解
      for (Annotation methodAnnotation : method.getAnnotations()) {
        processAnnotationOnMethod(data, methodAnnotation, method);
      }
    
      if (data.isIgnored()) {
        return data;
      }
     //获取方法参数类型
      Class<?>[] parameterTypes = method.getParameterTypes();
     //获取方法泛型类型
      Type[] genericParameterTypes = method.getGenericParameterTypes();
	 //获取方法的参数注解,是二维数组,因为每个参数可以有多个注解,没有注解则为空数组
      Annotation[][] parameterAnnotations = method.getParameterAnnotations();
      int count = parameterAnnotations.length;
      for (int i = 0; i < count; i++) {
        boolean isHttpAnnotation = false;
        //解析参数的注解
        if (parameterAnnotations[i] != null) {
          isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
        }

        if (isHttpAnnotation) {
          data.ignoreParamater(i);
        }
	
        if (parameterTypes[i] == URI.class) {
          data.urlIndex(i);
        } else if (!isHttpAnnotation && parameterTypes[i] != Request.Options.class
            //如果第i位参数没有被引用过,就将它设为bodyIndex
            //这里只有没注解的参数才能验证成功
              && !data.isAlreadyProcessed(i)) {
            //在设置bodyIndex前保证formParams是空的,也就是没有无引用的@Param标记的参数
          checkState(data.formParams().isEmpty(),
              "Body parameters cannot be used with form parameters.");
          checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
          data.bodyIndex(i);
          data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
        }
      }

      if (data.headerMapIndex() != null) {
        checkMapString("HeaderMap", parameterTypes[data.headerMapIndex()],
            genericParameterTypes[data.headerMapIndex()]);
      }

      if (data.queryMapIndex() != null) {
        if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) {
          checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]);
        }
      }

      return data;
    }

默认的encoder只能处理String和byte[]类型的参数,其他参数需要自定义encoder来处理。

如下面两种情况,存在没注解的参数,会把他们转成请求体使用:

1
2
3
4
    interface TestJson {
        @RequestLine(value = "POST /test")
        Response getItemCoupon(String name);
    }
1
2
3
4
    interface TestJson {
        @RequestLine(value = "POST /test")
        Response getItemCoupon(Map<String,Object> name);
    }

需要注意的一点是,这种方式构建请求body和上面@Param注解注意事项里面未使用的@Param标记构建请求body的方式一起存在的话,有两种可能。

如果@Param的参数在无注解参数的前面,则按照上面代码49行会抛出Body parameters cannot be used with form parameters,提示body参数不能和表单参数一起用。

如果无注解参数放在@Param参数前面,无注解方式会被忽略,发送请求体由@Param的参数决定。

三种设置请求体的优先级

一共三种设置请求体的方式,分别是:

  • 1.使用@Body注解
  • 2.使用没有任何注解的参数 (这样的参数只能存在一个)
  • 3.使用没有被使用的@Param标记的参数 (这个可以有多个参数)

他们之间的关系很复杂不能简单的用优先级来排序了。

1
2
3
4
5
6
7
8
      super.registerMethodAnnotation(Body.class, (ann, data) -> {
        String body = ann.value();
        if (body.indexOf('{') == -1) {
          data.template().body(body);
        } else {
          data.template().bodyTemplate(body);
        }
      });

可以看到,如果有body注解,如果body注解的值是固定的,则将值设置为body,如果是可变的设置为bodyTemplate

根据上面Param注解注意事项源码看出,如果@Param标记的参数没被使用,则放入data.formParams()

在根据上面讲的没有加任何注解的参数, 则会将他设置为data.bodyIndex(i)

且bodyIndex只能设置一个,设置bodyIndex时会断言formParams为空,所以后两个同时使用的话,要保证后者放在参数的前面,先解析才不会报错。

调用时是这样的:feign.ReflectiveFeign.ParseHandlersByName#apply

1
2
3
4
5
6
7
8
9
10
11
//如果formarams中有值,bodyTemplate没值,则是哟个formarams构建请求体
if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
          buildTemplate =
              new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
//否则尝试是哟个bodyIndex构建请求体    
        } else if (md.bodyIndex() != null) {
          buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target);
//否则使用默认bodyTemplate构建请求体
        } else {
          buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target);
        }

所以他们的优先级是

formParams > bodyIndex

bodyIndex优先级 > bodyTemplate

bodyTemplate > formParams

简直是个三角形的关系。

但我觉得将无注解参数放在@Param参数前就不报错,应该数据bug。


转载请注明出处:https://www.huangchaoyu.com/2019/12/17/openFeign-高级功能/