Feign NB

Feign高级功能

  • TOC
    {:toc}

动态修改请求地址

像这样,创建接口时放置一个类型为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。


Feign高级功能
https://www.huangchaoyu.com/1305379791.html
作者
hcy
发布于
2019年12月17日
更新于
2024年8月17日
许可协议