feign form支持

来源:互联网 发布:淘宝静物拍摄相机 编辑:程序博客网 时间:2024/06/01 21:57

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);

第一次扩展没中文文档的开源框架,英语不好,水平有限,理解有问题的地方请不吝指出。