使用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</artifactId>
<version>3.2.2</version>
</dependency>
<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;
}
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