feign是一个非常好用的http客户端工具,简单入门请见上篇文章,不多做介绍
但是在使用feign的时候也碰到了一点小坑,今天就来讲讲怎么解决这个坑
feign bean提交
看官方文档,feign post提交的时候可以使用bean传输,不需要每个参数注解@Param,然而feign会把这个bean的内容写入到http的 body中去。contentType为applicationJson
spring mvc接收需要在接口对应的bean上注解@RequestBody,从body中读取这个bean的内容。
如下
@Headers("Content-Type: application/json")
@RequestLine("POST /test1")
public BaseResponse test1(Demo demo);
@ResponseBody
@RequestMapping(value = "/test1", method = RequestMethod.POST)
public BaseResponse test1(@RequestBody Demo demo) {
return BaseResponse.SUCCESS_RESPONSE;
}
使用一直很舒畅,因为自己的项目都是restful风格的接口
直到调用公司其他的项目组接口发现完蛋了,对方不是用body接收复杂对象
临时方案
为了解决这种问题只能使用@Param了,但是参数多的时候会写很长的方法参数
@Headers("Content-Type: application/x-www-form-urlencoded")
@RequestLine("POST /test2")
public BaseResponse test2(@Param("id") int id,@Param("name")String name);
@ResponseBody
@RequestMapping(value = "/test2", method = RequestMethod.POST)
public BaseResponse test2(Demo demo) {
return BaseResponse.SUCCESS_RESPONSE;
}
另一种方式就是使用@QueryMap了
@Headers("Content-Type: application/x-www-form-urlencoded")
@RequestLine("POST /test2")
public BaseResponse test3(@QueryMap Map<String,Object> param);
这样暂时是对付过去了,但是总归不是很优雅。
扩展feign
仔细研究feign的源码后,发现feign是根据目标接口生成代理对象,生成代理对象的过程中会根据接口方法生成一个MethodMetadata对象,其中封装了方法签名configKey,form表单参数列表formParams,参数index对应的参数名indexToName等。属性如下:
private String configKey;
private transient Type returnType;
private Integer urlIndex;
private Integer bodyIndex;
private Integer headerMapIndex;
private Integer queryMapIndex;
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;
而生成这个对象的类是Contract,可以在Feign构造器中设置。
可以自己扩展Contract,将复杂对象的参数名设置进indexToName就行了,这里虽然是int->集合的类型。但是在调用我们远程接口时,feign会将我们的参数转化为param->value的map形式。而feign在转换的过程中,如果indexToName index对应的name有多个的话,会迭代这个collection,然后讲传入的参数设置进去,并不会解析其中的属性,如下:
@Override
public RequestTemplate create(Object[] argv) {
RequestTemplate mutable = new RequestTemplate(metadata.template());
...
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.
...
for (String name : entry.getValue()) {
varBuilder.put(name, value);
}
}
}
...
return template;
}
以class Demo{ int id,String name} 为例,如果indexToName为 0->[“id”,”name”],最后解析出来就是
{“id”:“{id:1,name:chen}”,“name”“{id:1,name:chen}”};根本不是我们想要的结果
最后灵机一动,直接将参数名设为一个固定字符串就行,反正转换调用的参数时可以获得属性名
方案如下:
1.将带有@FormBean标注的参数的参数名定义为@FORM@+index,indexToName中为index->”@FORM@index”
“@FORM@”为自定义的一个特殊字符,怕冲突可以使用class的hashcode
2.调用时,在encoder中将参数名为”@FORM@”开头的参数删除,将传入的参数转换为map,添加到参数键值对中
public class FormContract extends Contract.Default {
public static String FORM_PARAM_NAME="@FORM@";
private String formParamName;
public FormContract() {
this(FORM_PARAM_NAME);
}
public FormContract(String formParamName) {
this.formParamName = formParamName;
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
boolean isHttpAnnotation = super.processAnnotationsOnParameter(data, annotations, paramIndex);
for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotationType == FormBean.class) {
FormBean paramAnnotation = (FormBean) annotation;
//注解了FormBean 的参数名定义为@FORM@+index
String name=formParamName+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;
String varName = '{' + name + '}';
if (!data.template().url().contains(varName) &&
!searchMapValuesContainsSubstring(data.template().queries(), varName) &&
!searchMapValuesContainsSubstring(data.template().headers(), varName)) {
data.formParams().add(name);
}
}
}
return isHttpAnnotation;
}
private static <K, V> boolean searchMapValuesContainsSubstring(Map<K, Collection<String>> map,String search) {
......
}
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template) {
......
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) object;
formObjectExpand(data);
processors.get(formType).process(data, template);
}
private void formObjectExpand(Map<String, Object> data) {
List<String> readyRemove=new ArrayList<>();
Map<String,Object> insert=new HashMap<>();
for(String key:data.keySet()){
if(key.startsWith(FORM_PARAM_NAME)){
Object value = data.get(key);
readyRemove.add(key);
try {
insert.putAll(objectConvertor.toMap(value));
} catch (ConvertException e) {
LoggerUtil.logException(e);
}
}
}
//ConcurrentModificationException
data.putAll(insert);
for (String key : readyRemove) {
data.remove(key);
}
}
具体例子请见我的github代码 FormContract和FormEncoder
生成feign客户端的方式请见FeinFactory
调用代码变为
@Headers("Content-Type: application/x-www-form-urlencoded")
@RequestLine("POST /test2")
public BaseResponse test4(@FormBean Demo demo);
第一次扩展没中文文档的开源框架,英语不好,水平有限,理解有问题的地方请不吝指出。