feign form支持

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代码 FormContractFormEncoder
生成feign客户端的方式请见FeinFactory

调用代码变为

    @Headers("Content-Type: application/x-www-form-urlencoded")
    @RequestLine("POST /test2")
    public BaseResponse test4(@FormBean Demo demo);

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值