Spring 提供了封装比较完善的RestTemplate进行请求交互,但对于响应码非200(非正常)的响应的想要查看返回body的信息,用起来有那么一些不方便(后续证实并非我一个人这么觉得,在github上成功找到了共鸣)
问题
先说不方便之处:
工作汇总调用其他接口返回非正常响应,异常message里面只有响应码和固定文案,比较难排查失败原因,失败原因通常存放在响应body中。
RestTemplate对于400系列,500系列的响应,抛出了内部封装的异常,响应body不在异常message中,以字节数组的方式存在了所抛异常的body字段中,排查起来,还要专门取用。
源码跟踪
我们从最常见的请求调用exchange()看起,进源码里面闯荡一番,跟着我一起看一下就知道了
ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.POST, entity, String.class);
然后顺着exchange(),查看发请求的核心内容,一路摸到了RestTemplate.doExecute()方法,源码如下:
/**
* Execute the given method on the provided URI.
* <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback};
* the response with the {@link ResponseExtractor}.
* @param url the fully-expanded URL to connect to
* @param method the HTTP method to execute (GET, POST, etc.)
* @param requestCallback object that prepares the request (can be {@code null})
* @param responseExtractor object that extracts the return value from the response (can be {@code null})
* @return an arbitrary object, as returned by the {@link ResponseExtractor}
*/
@Nullable
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
Assert.notNull(url, "URI is required");
Assert.notNull(method, "HttpMethod is required");
ClientHttpResponse response = null;
try {
ClientHttpRequest request = createRequest(url, method);
if (requestCallback != null) {
requestCallback.doWithRequest(request);
}
response = request.execute();
handleResponse(url, method, response);
return (responseExtractor != null ? responseExtractor.extractData(response) : null);
}
catch (IOException ex) {
String resource = url.toString();
String query = url.getRawQuery();
resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
throw new ResourceAccessException("I/O error on " + method.name() +
" request for \"" + resource + "\": " + ex.getMessage(), ex);
}
finally {
if (response != null) {
response.close();
}
}
}
核心处理在try包裹的代码部分,一开始构建了一下request,然后调了execute()接口,然后拿到响应数据response,进handleResponse()内进行处理。好,顺着响应处理进去
/**
* Handle the given response, performing appropriate logging and
* invoking the {@link ResponseErrorHandler} if necessary.
* <p>Can be overridden in subclasses.
* @param url the fully-expanded URL to connect to
* @param method the HTTP method to execute (GET, POST, etc.)
* @param response the resulting {@link ClientHttpResponse}
* @throws IOException if propagated from {@link ResponseErrorHandler}
* @since 4.1.6
* @see #setErrorHandler
*/
protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
ResponseErrorHandler errorHandler = getErrorHandler();
boolean hasError = errorHandler.hasError(response);
if (logger.isDebugEnabled()) {
try {
int code = response.getRawStatusCode();
HttpStatus status = HttpStatus.resolve(code);
logger.debug("Response " + (status != null ? status : code));
}
catch (IOException ex) {
// ignore
}
}
if (hasError) {
errorHandler.handleError(url, method, response);
}
}
分析一下,首先取了一下ResponseErrorHandler对象,即异常处理器对象,RestTemplate中默认的异常处理器是DefaultResponseErrorHandler
然后,判断了一下响应是否是正常响应,4系列和5系列响应码,被视为非正常响应
/**
* Whether this status code is in the HTTP series
* {@link org.springframework.http.HttpStatus.Series#CLIENT_ERROR} or
* {@link org.springframework.http.HttpStatus.Series#SERVER_ERROR}.
* This is a shortcut for checking the value of {@link #series()}.
* @since 5.0
* @see #is4xxClientError()
* @see #is5xxServerError()
*/
public boolean isError() {
return (is4xxClientError() || is5xxServerError());
}
接着,这个是否正常响应的布尔标志,被作为是否进行异常处理的if条件,顺着异常处理的handleError()进去,到达DefaultResponseErrorHandler.handleError()方法
/**
* Handle the error in the given response with the given resolved status code.
* <p>The default implementation throws an {@link HttpClientErrorException}
* if the status code is {@link HttpStatus.Series#CLIENT_ERROR}, an
* {@link HttpServerErrorException} if it is {@link HttpStatus.Series#SERVER_ERROR},
* and an {@link UnknownHttpStatusCodeException} in other cases.
* @since 5.0
* @see HttpClientErrorException#create
* @see HttpServerErrorException#create
*/
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = getResponseBody(response);
Charset charset = getCharset(response);
switch (statusCode.series()) {
case CLIENT_ERROR:
throw HttpClientErrorException.create(statusCode, statusText, headers, body, charset);
case SERVER_ERROR:
throw HttpServerErrorException.create(statusCode, statusText, headers, body, charset);
default:
throw new UnknownHttpStatusCodeException(statusCode.value(), statusText, headers, body, charset);
}
}
可以看到,根据状态码,400系列会抛出HttpClientErrorException,500系列会抛出HttpServerErrorException,然后从流中获取的字节数组body没有放置到异常的message字段中,而是专门有个body字段存储,但是还是字节数组格式
解决方案
-
捕获抛出的HttpClientErrorException、HttpServerErrorException,取出body字段转String
-
重写handleError()方法,自定义处理逻辑,如下:
restTemplate.setErrorHandler((new DefaultResponseErrorHandler() { @Override public void handleError(ClientHttpResponse response) throws IOException { if(response.getStatusCode().is4xxClientError() || response.getStatusCode().is5xxServerError()){ //特殊处理 }else{ //交给RestTemplate处理 super.handleError(response); } };
博主比较推荐第三种解决方案,在下面🤭
探索之旅
问题如果到这里,就是个普普通通的知识点,但是,后来整理编写博客时,打开了另一个自己的测试项目,看到同样的源码处,发现点不一样的地方,发现我测试项目的版本为:Spring-web:5.3.15
/**
* Handle the error based on the resolved status code.
*
* <p>The default implementation delegates to
* {@link HttpClientErrorException#create} for errors in the 4xx range, to
* {@link HttpServerErrorException#create} for errors in the 5xx range,
* or otherwise raises {@link UnknownHttpStatusCodeException}.
* @since 5.0
* @see HttpClientErrorException#create
* @see HttpServerErrorException#create
*/
protected void handleError(ClientHttpResponse response, HttpStatus statusCode) throws IOException {
String statusText = response.getStatusText();
HttpHeaders headers = response.getHeaders();
byte[] body = getResponseBody(response);
Charset charset = getCharset(response);
String message = getErrorMessage(statusCode.value(), statusText, body, charset);
switch (statusCode.series()) {
case CLIENT_ERROR:
throw HttpClientErrorException.create(message, statusCode, statusText, headers, body, charset);
case SERVER_ERROR:
throw HttpServerErrorException.create(message, statusCode, statusText, headers, body, charset);
default:
throw new UnknownHttpStatusCodeException(message, statusCode.value(), statusText, headers, body, charset);
}
}
恰好发现这块和之前看的低版本有了一些差别,在抛异常之前,多了一步message的处理,即getErrorMessage(),进去一探究竟
/**
* Return error message with details from the response body. For example:
* <pre>
* 404 Not Found: [{'id': 123, 'message': 'my message'}]
* </pre>
*/
private String getErrorMessage(
int rawStatusCode, String statusText, @Nullable byte[] responseBody, @Nullable Charset charset) {
String preface = rawStatusCode + " " + statusText + ": ";
if (ObjectUtils.isEmpty(responseBody)) {
return preface + "[no body]";
}
charset = (charset != null ? charset : StandardCharsets.UTF_8);
String bodyText = new String(responseBody, charset);
bodyText = LogFormatUtils.formatValue(bodyText, -1, true);
return preface + bodyText;
}
就很清晰明了了,字节数组body被转为String,与状态码 + 状态码对应原因 拼接在了一起作为了异常message
这就很方便了,直接不用区分是什么异常,直接从异常message中便可以看到body内容了
继续探索,去github上 看看版本更新日志,终于发现,Spring 5.3.11更新处理这一块
https://github.com/spring-projects/spring-framework/releases?page=3
翻译过来便是:默认响应错误处理程序允许记录完整的错误响应正文
然后点进对应issue看看,https://github.com/spring-projects/spring-framework/issues/27552
哈哈🤣🤣,这位老哥和我是一样的感受,当用RestTemplate发起请求,并返回4xx或5xx的异常以及body信息时,如果只是捕获记录Exception不能记录完整的body信息
意外之喜
所以,还有第三种解决方案,至少升级至Spring-web:5.3.11
😂😂😂,就能直接在抛出的异常message中查看到body内容了