第一次遇到HttpClientErrorException: 400时,我盯着控制台的红字一脸懵——明明代码能编译通过,为什么服务器就是不认我的请求?后来才发现,这就像给朋友发微信,如果打错字或者漏说关键信息,对方也会回个"?"。400错误就是服务器在向我们表达这种困惑。
RestTemplate作为Spring生态中的HTTP客户端老将,用起来简单但坑也不少。最常见的问题集中在三个维度:
请求体格式:比如把JSON字符串的引号写成中文标点,或者日期格式不符合API要求。有次我遇到个隐蔽问题:用FastJson序列化的对象包含isActive字段,但服务端用的是Jackson,结果因为字段名序列化差异导致400错误。
URL构造:手动拼接URL时容易漏编码特殊字符。比如参数值包含&符号时,如果不做URL编码就会破坏整个查询字符串结构。更稳妥的做法是用UriComponentsBuilder:
java复制String url = UriComponentsBuilder.fromHttpUrl("http://api.example.com/search")
.queryParam("q", "Java&Spring") // 自动编码特殊字符
.build()
.toUriString();
application/json,实际测试发现必须用application/json;charset=UTF-8才不会报400。这类问题只能通过抓包对比成功和失败的请求才能发现。在请求发出前做校验就像出门前检查"手机、钱包、钥匙",能避免很多低级错误。我习惯为不同API定义校验规则:
java复制public void validateRequest(MyRequestDto dto) {
Objects.requireNonNull(dto.getUserId(), "用户ID不能为空");
if (dto.getAmount() <= 0) {
throw new IllegalArgumentException("金额必须大于零");
}
// 更复杂的业务规则校验...
}
对于JSON请求体,可以用JSON Schema做结构化校验。引入everit-json-schema库后,可以这样定义校验规则:
java复制SchemaLoader loader = SchemaLoader.builder()
.schemaJson(jsonSchema)
.build();
Schema schema = loader.load().build();
schema.validate(new JSONObject(requestBody)); // 抛出ValidationException时说明格式非法
直接抛出原始400异常对调用方很不友好。我们应该解析响应体,提取服务端返回的具体错误信息。比如很多REST API会返回这样的错误结构:
json复制{
"errorCode": "INVALID_PARAM",
"message": "金额字段格式不正确"
}
可以自定义异常处理器:
java复制@ExceptionHandler(HttpClientErrorException.class)
public ErrorResult handleBadRequest(HttpClientErrorException ex) {
if (ex.getStatusCode() == HttpStatus.BAD_REQUEST) {
String responseBody = ex.getResponseBodyAsString();
// 解析响应体中的错误详情
return objectMapper.readValue(responseBody, ErrorResult.class);
}
// 其他状态码处理...
}
遇到问题时,完整的请求日志能节省大量排查时间。我通常会配置一个ClientHttpRequestInterceptor:
java复制public class RequestLoggingInterceptor implements ClientHttpRequestInterceptor {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingInterceptor.class);
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
log.debug("请求URL: {}", request.getURI());
log.debug("请求头: {}", request.getHeaders());
log.debug("请求体: {}", new String(body, StandardCharsets.UTF_8));
ClientHttpResponse response = execution.execute(request, body);
log.debug("响应状态: {}", response.getStatusCode());
log.debug("响应头: {}", response.getHeaders());
log.debug("响应体: {}", StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8));
return response;
}
}
注意在生产环境要过滤敏感信息,比如Authorization头和密码字段。
有些400错误可能是暂时的,比如服务端正在部署或者网络抖动。可以为特定错误实现有条件重试:
java复制@Retryable(value = HttpClientErrorException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 1000),
exclude = {BadCredentialsException.class}) // 认证错误不应重试
public String callApiWithRetry(String url, HttpEntity<?> entity) {
return restTemplate.postForObject(url, entity, String.class);
}
不要在每个服务里重复创建RestTemplate实例。推荐通过RestTemplateBuilder集中配置:
java复制@Bean
public RestTemplate secureRestTemplate(RestTemplateBuilder builder) {
return builder
.rootUri("https://api.example.com")
.additionalInterceptors(new RequestLoggingInterceptor())
.errorHandler(new CustomErrorHandler())
.requestFactory(this::bufferingRequestFactory)
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(10))
.build();
}
private ClientHttpRequestFactory bufferingRequestFactory() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setBufferRequestBody(true); // 支持重复读取请求体
return factory;
}
在微服务架构下,可以用Spring Cloud Contract做契约测试。先在服务端定义契约:
groovy复制Contract.make {
request {
method 'POST'
url '/orders'
body([
productId: $(regex('[0-9a-f]{8}')),
quantity: $(regex('[1-9][0-9]*'))
])
headers {
contentType(applicationJson())
}
}
response {
status 201
}
}
然后在客户端测试中验证请求是否符合契约,避免因接口变更导致400错误。
通过Micrometer暴露关键指标:
java复制@Bean
public MeterBinder restTemplateMetrics(RestTemplate restTemplate) {
return new RestTemplateExchangeTagsProvider().bindTo(registry -> {
Timer.builder("http.client.requests")
.description("RestTemplate请求耗时")
.tags("status", "400")
.register(registry);
});
}
在Grafana中设置当400错误率超过5%时触发告警,帮助及时发现接口问题。
真正健壮的API客户端应该像老司机开车——既知道如何应对突发状况,更懂得提前预防风险。我在项目中推行过这些实践:
有一次我们接入支付接口时,通过预校验发现了文档没写明的字段长度限制,避免了上线后的批量失败。这种防御性思维的价值,往往在关键时刻显现。