有个接口是这样的getXxByIds(String Ids)
id用','分隔,运行一段时间报了400。
报错内容
feign.FeignException$BadRequest: [400] during [POST] to [http...
问题重现
来模拟下问题
服务接口
@Slf4j
@RestController
public 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
@SpringBootTest
class 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方式调用服务。