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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章