RestTemplate 非正常响应处理 探索

527 阅读5分钟

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字段存储,但是还是字节数组格式

解决方案

  1. 捕获抛出的HttpClientErrorException、HttpServerErrorException,取出body字段转String

  2. 重写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更新处理这一块

github.com/spring-proj…

image.png

翻译过来便是:默认响应错误处理程序允许记录完整的错误响应正文

然后点进对应issue看看,github.com/spring-proj…

image.png

哈哈🤣🤣,这位老哥和我是一样的感受,当用RestTemplate发起请求,并返回4xx或5xx的异常以及body信息时,如果只是捕获记录Exception不能记录完整的body信息

意外之喜

所以,还有第三种解决方案,至少升级至Spring-web:5.3.11 😂😂😂,就可以直接从异常message中看到body内容了