有个接口是这样的getXxByIds(String Ids) id 用','分隔,运行一段时间报了 400。
报错内容
feign.FeignException$BadRequest: [400] during [POST] to [http...
复制代码
问题重现
来模拟下问题
服务接口
@Slf4j@RestControllerpublic class SkyController {
@GetMapping("/sky") public String sky (@RequestParam(required = false) String name) { log.info(name); return name; }
@PostMapping("/deliver") public String deliver (String packageBox) { log.info(packageBox); return packageBox; }
@PostMapping("/feignPost") public String feignPost(@RequestParam(name = "name") String name){ log.info(name); return name; }
@PostMapping("/feignBody") public String feignBody(@RequestBody String name){ log.info(name); return name; }
}
复制代码
另建一个服务 创建 feignClinet, 示例服务加入了网关,使用了服务名称,可以直接使用 url
// 网关 服务名称@FeignClient(value = "paw-dogs-sky-service")public interface SkyFeignClient {
@PostMapping("/deliver") String deliver (@RequestParam(name = "packageBox") String packageBox);
@PostMapping("/feignPost") String feignPost(@RequestParam(name = "name") String name);
@PostMapping("/feignBody") String feignBody(@RequestBody String name);}
复制代码
编写测试类
@Slf4j@SpringBootTestclass SkyFeignClientTest {
@Autowired SkyFeignClient skyFeignClient;
@Test void deliver () { String param = RandomUtil.randomString(10*1024); log.info(param); String result = skyFeignClient.deliver(param); log.info(result);
}
@Test void feignPost () { String param = RandomUtil.randomString(10*1024); log.info(param); String result = skyFeignClient.feignPost(param); log.info(result); }
@Test void feignBody () { String param = RandomUtil.randomString(1*1024); log.info(param); String result = skyFeignClient.feignBody(param); log.info(result); }}
复制代码
运行测试发现 param 较大时 get、post form-url 请求失败,requestBody 方式请求成功。
运行 post form-url 请求 会发现 post 请求发送的也是 url 拼接的方式
用 postman 直接访问服务测试
通过拼接的长 url 访问失败
通过 form-url 方式访问成功
问题出现在 url 的长度限制
url 长度限制
浏览器 url 长度限制
服务器长度限制 如 tomcat 限制, nginx url 限制
SpringBoot 项目长度限制 max-http-header-size 默认 8k,更改提供服务 sky 的配置 100*1024 (100k) ,重试服务正常调用。
DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8)
server: port: 8080 max-http-header-size: 102400
复制代码
源码分析
从异常类 SynchronousMethodHandler 入手 debug 跟踪
构建请求的类 RequestTemplate 对应 query 请求 用有序链表存放参数 Map<String, QueryTemplate> queries = new LinkedHashMap<>() 形如 key-->value packageBox--> packageBox={packageBox}
SpringMvcContract 对注解进行处理
类上的注解processAnnotationOnClass
方法上的注解processAnnotationOnMethod
参数上的注解 processAnnotationsOnParameter
AnnotatedParameterProcessor 的实现类 RequestParamParameterProcessor 对 query 参数进行封装
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) { int parameterIndex = context.getParameterIndex(); Class<?> parameterType = method.getParameterTypes()[parameterIndex]; MethodMetadata data = context.getMethodMetadata();
if (Map.class.isAssignableFrom(parameterType)) { checkState(data.queryMapIndex() == null, "Query map can only be present once."); data.queryMapIndex(parameterIndex);
return true; }
RequestParam requestParam = ANNOTATION.cast(annotation); String name = requestParam.value(); checkState(emptyToNull(name) != null, "RequestParam.value() was empty on parameter %s", parameterIndex); context.setParameterName(name);
// 对参数进行封装 Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name)); data.template().query(name, query); return true; }
复制代码
SynchronousMethodHandler invoke 方法 executeAndDecode(template, options) 执行 http 请求并解码。
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable { // 构建请求 Request request = targetRequest(template);
if (logLevel != Logger.Level.NONE) { logger.logRequest(metadata.configKey(), logLevel, request); }
Response response; long start = System.nanoTime(); try { // 执行http请求 response = client.execute(request, options); ... }
复制代码
构建 Request 请求,通过 client 访问 http 服务。实现的 client 有Default LoadBalancerFeignClient FeignBlockingLoadBalancerClient
解决
1. 调大服务提供者的header参数(微服务较多 不太适用) 2. 改为requestBody调用服务
复制代码
总结
feign 通过解析接口类、方法、参数上的注解,通过RequestTemplate @RequestParam 以 Url 拼接的方式,构建了Request请求,通过 Client 访问 http 服务。对较长参数改为 RequestBody 方式调用服务。
评论