RestTemplate 异常响应处理 源码探索

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更新处理这一块

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内容了

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南窗木心

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值