版本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