feign以form方式提交多个参数

版本springboot2,feign9.5.1

项目关系:gateway以feignCient形式调用聊天室项目chatclient

起因:chatclient项目非常简单,但是内存监控发现内存却占用很高,jstat一直在fullgc,没有ygc,年轻代from和to都为0。

初步处理:jvm参数中设置年轻代小一点,-Xmx1024m -Xmn168m,gc回收正常了,内存降了两三百M,但是还是高的不正常,老年代占用非常高

排查原因:跟其他项目比对一下,有几个特别之处,接入了配置中心、有一个每隔15ms加了@Async的任务消费Rabbitmq,等等,删除这些以后内存没有变化。启动项目时,老年代从50m经过一次fullgc一下增长到80m,启动后第一次访问过后,再突增到120m。经过各种试验,注释代码,最终找到原因,是两行配置

server.max-http-header-size=20480000
server.tomcat.max-http-post-size=20480000

这个是20m,去掉以后内存正常了,改小一点也可以。

之前线上发送请求比较大,几十kb的时候gateway报错400,所以在chatclient项目中加入配置解决这个问题。

问题没完继续追查:在本地搭起eureka-server、gateway、chatclient,重现请求超大400bug,进入错误日志报错行进行debug,发现请求是GET,所有参数都拼在url后面,url太长了,报错400,把url拷出来,用RestTemplate请求,同样出现400错误,至此确定是请求方式原因。如果改用post,那两行配置不需使用,也就不会出现内存问题。

FeignClient的写法有问题,原来写法如下,@RequestParam的字段都会拼在url后面

@RequestMapping("send2User")
	String send2User(@RequestParam("sign")String sign,
						@RequestParam("fromUsername")String fromUsername,
						@RequestParam("toUsername")String toUsername,
						@RequestParam("content")String content,
						@RequestParam("isImmediately")Integer isImmediately,
						@RequestParam("ext")String ext);

chatclient项目接口如下

@RequestMapping("send2User")
	public Result send2User(String fromUsername, String toUsername, String content, Integer isImmediately, String ext)

修改步骤:

如果只修改@PostMapping,没有作用,还是所有参数都拼在url后面

一种方法是用一个map包装起来

@RequestBody HashMap<String, String> map

同步要修改gateway的controller,将参数放到map里面,调用client。还要修改chatclient接口,参数改为map。不想修改后者,经过一番搜索,最终找到实现方法,修改步骤如下:

添加依赖,这两个是feign团队扩展的jar包,用于form提交的


<dependency>
  <groupId>io.github.openfeign.form</groupId>
  <artifactId>feign-form-spring</artifactId>
  <version>3.2.2</version>
</dependency>

创建一个Encoder,其中关键一句是:super.encode(data, MAP_STRING_WILDCARD, template);


import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.form.spring.SpringFormEncoder;

public class MyEncoder extends SpringFormEncoder{

	public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
		
		if (((ParameterizedTypeImpl) bodyType).getRawType().equals(Map.class)) {
            Map<String,?> data = (Map<String, ?>) object;
            Set<String> nullSet = new HashSet<>();
            for (Map.Entry<String, ?> entry : data.entrySet()) {
                if (entry.getValue() == null) {
                    nullSet.add(entry.getKey());
                }
            }
            for (String s : nullSet) {
                data.remove(s);
            }
            super.encode(data, MAP_STRING_WILDCARD, template);
//            int i=1/0;
            return;
        }
		super.encode(object, bodyType, template);
	}
}

加一句int i=1/0;可以debug找出源码调用的流程

创建一个config,引入自定义的Encoder

import org.springframework.context.annotation.Bean;

import feign.codec.Encoder;

public class FormSupportConfig {

	@Bean
	public Encoder feignFormEncoder() {
		return new MyEncoder();
//		return new SpringFormEncoder();
	}
	
}

client的注解加上config

@FeignClient(value="chatclient",path="chatclient",configuration=FormSupportConfig.class)

方法修改为map参数

@PostMapping(value = "send2User", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE })
String send2User(Map<String, String> map);

gateway的controller把参数包装成map

chatclient的controller不用修改

feignclient中其他方法不用修改,可以兼容

源码分析:

1:应用启动时初始化各个feignclient,处理类:feign.Contract,给每个方法组装成一个MethodMetadata,里面有一个RequestTemplate,记录url,post/get,读取方法注解上的consumes作为content-type

metadata中有个bodyIndex,如果是@RequestParam,这个字段为空,map提交的话则为0。请求时根据这个字段寻找处理类

2:请求处理类feign.ReflectiveFeign

这里面有一个内部类class FeignInvocationHandler implements InvocationHandler,表明使用的jdk动态代理的方式

类FeignInvocationHandler的invoke方法

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object
              otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }
      return dispatch.get(method).invoke(args);
    }

dispatch.get(method)返回的是一个feign.SynchronousMethodHandler对象,其中的invoke方法如下

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template);
      } catch (RetryableException e) {
        retryer.continueOrPropagate(e);
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

核心是创建一个RequestTemplate

其中buildTemplateFromArgs.create(argv)实现类有三个,都是ReflectiveFeign的内部类

普通的Get方法对应的是BuildTemplateByResolvingArgs

修改后的map提交方法对应的是BuildEncodedTemplateFromArgs

选择处理类的逻辑在内部类ParseHandlersByName中,如下

public Map<String, MethodHandler> apply(Target key) {
      List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
      Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
      for (MethodMetadata md : metadata) {
        BuildTemplateByResolvingArgs buildTemplate;
        if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
          buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder);
        } else if (md.bodyIndex() != null) {
          buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder);
        } else {
          buildTemplate = new BuildTemplateByResolvingArgs(md);
        }
        result.put(md.configKey(),
                   factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
      }
      return result;
    }

BuildTemplateByResolvingArgs中的create方法,其中会把参数组装map,调用resolve。

 

public RequestTemplate create(Object[] argv) {
      RequestTemplate mutable = new RequestTemplate(metadata.template());
      if (metadata.urlIndex() != null) {
        int urlIndex = metadata.urlIndex();
        checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
        mutable.insert(0, String.valueOf(argv[urlIndex]));
      }
      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.
          if (indexToExpander.containsKey(i)) {
            value = expandElements(indexToExpander.get(i), value);
          }
          for (String name : entry.getValue()) {
            varBuilder.put(name, value);
          }
        }
      }

      RequestTemplate template = resolve(argv, mutable, varBuilder);
      if (metadata.queryMapIndex() != null) {
        // add query map parameters after initial resolve so that they take
        // precedence over any predefined values
        template = addQueryMapQueryParameters((Map<String, Object>) argv[metadata.queryMapIndex()], template);
      }

      if (metadata.headerMapIndex() != null) {
        template = addHeaderMapHeaders((Map<String, Object>) argv[metadata.headerMapIndex()], template);
      }

      return template;
    }

BuildEncodedTemplateFromArgs继承了BuildTemplateByResolvingArgs,复用create 方法,重写了resolve方法

private static class BuildEncodedTemplateFromArgs extends BuildTemplateByResolvingArgs {

    private final Encoder encoder;

    private BuildEncodedTemplateFromArgs(MethodMetadata metadata, Encoder encoder) {
      super(metadata);
      this.encoder = encoder;
    }

    @Override
    protected RequestTemplate resolve(Object[] argv, RequestTemplate mutable,
                                      Map<String, Object> variables) {
      Object body = argv[metadata.bodyIndex()];
      checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());
      try {
        encoder.encode(body, metadata.bodyType(), mutable);
      } catch (EncodeException e) {
        throw e;
      } catch (RuntimeException e) {
        throw new EncodeException(e.getMessage(), e);
      }
      return super.resolve(argv, mutable, variables);
    }
  }

这里encoder就是自定义的encoder

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值