Spring RestTemplate 处理状态为 NO_CONTENT 的响应时的行为

问题描述 投票:0回答:7

好吧,我有一个 NamedSystems 类,它的唯一字段是一组 NamedSystem。

我有一种方法可以根据某些标准查找命名系统。这并不重要。当得到结果时,一切都会正常进行。然而,当它找不到任何东西,从而返回一个空(或空——我已经尝试了两种方法)集时,我就会遇到问题。让我解释一下。

我正在使用 Spring RestTemplate 类,并且我在单元测试中进行如下调用:

ResponseEntity<?> responseEntity = template.exchange(BASE_SERVICE_URL + "?
  alias={aliasValue}&aliasAuthority={aliasAssigningAuthority}", 
  HttpMethod.GET, makeHttpEntity("xml"), NamedSystems.class, 
  alias1.getAlias(), alias1.getAuthority());

现在,由于这通常会返回 200,但我想返回 204,所以我的服务中有一个拦截器,用于确定 ModelAndView 是否是 NamedSystem 以及它的集合是否为 null。如果是这样,我会将状态代码设置为 NO_CONTENT (204)。

当我运行 junit 测试时,出现此错误:

org.springframework.web.client.RestClientException: Cannot extract response: no Content-Type found

将状态设置为 NO_CONTENT 似乎会擦除内容类型字段(当我想到这一点时,这确实有意义)。那么为什么它还要看它呢?

Spring的HttpMessageConverterExtractor extractData方法:

public T extractData(ClientHttpResponse response) throws IOException {
    MediaType contentType = response.getHeaders().getContentType();
    if (contentType == null) {
        throw new RestClientException("Cannot extract response: no Content-Type found");
    }
    for (HttpMessageConverter messageConverter : messageConverters) {
        if (messageConverter.canRead(responseType, contentType)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Reading [" + responseType.getName() + "] as \"" + contentType
                    +"\" using [" + messageConverter + "]");
            }
            return (T) messageConverter.read(this.responseType, response);
        }
    }
    throw new RestClientException(
        "Could not extract response: no suitable HttpMessageConverter found for response type [" +
        this.responseType.getName() + "] and content type [" + contentType + "]");
}

沿着链向上查找一下 Extractor 的设置位置,我来到了测试中使用的 RestTemplate 的 Exchange() 方法:

public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
  HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException {
    HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
    ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType);
    return execute(url, method, requestCallback, responseExtractor, uriVariables);
}

因此,它试图将由于交换调用提供的响应类型而导致的内容转换为无。如果我将responseType从NamedSystems.class更改为null,它会按预期工作。它不会尝试转换任何内容。如果我尝试将状态代码设置为 404,它也可以正常执行。

我是否被误导了,或者这看起来像是 RestTemplate 中的一个缺陷?当然,我现在使用的是 junit,所以我知道会发生什么,但是如果有人使用 RestTemplate 来调用它并且不知道服务调用的结果,他们自然会使用 NamedSystems 作为响应类型。然而,如果他们尝试进行没有元素的标准搜索,他们就会遇到这个令人讨厌的错误。

有没有办法解决这个问题而不覆盖任何 RestTemplate 的东西?我对这种情况的看法是否错误?请帮忙,因为我有点困惑。

java web-services unit-testing rest spring-mvc
7个回答
12
投票

解决此问题的另一种方法是将响应实体设为 null,如下所示。

  ResponseEntity<?> response = restTemplate.exchange("http://localhost:8080/myapp/user/{userID}",
                                                             HttpMethod.DELETE, 
                                                             requestEntity,
                                                             null,
                                                             userID);

如果您仍然需要响应标头,请尝试实现 ResponseErrorHandler。


10
投票

我相信您可能应该查看 ResponseExtractor 接口并在提供提取器实现的 RestTemplate 上调用 execute。 对我来说,这似乎是执行此操作的常见要求,因此已记录此内容:

https://jira.springsource.org/browse/SPR-8016

这是我之前准备的:

private class MyResponseExtractor extends HttpMessageConverterExtractor<MyEntity> {

    public MyResponseExtractor (Class<MyEntity> responseType,
      List<HttpMessageConverter<?>> messageConverters) {
        super(responseType, messageConverters);
    }

    @Override
    public MyEntity extractData(ClientHttpResponse response) throws IOException {

        MyEntity result;

        if (response.getStatusCode() == HttpStatus.OK) {
            result = super.extractData(response);
        } else {
            result = null;
        }

        return result;
    }
}

我已经测试过了,它似乎可以满足我的要求。

为了创建 ResponseExtractor 的实例,我调用构造函数并从已注入的 RestTemplate 实例传递转换器;

例如

ResponseExtractor<MyEntity> responseExtractor =
    new MyResponseExtractor(MyEntity.class, restTemplate.getMessageConverters());

那么调用的是:

MyEntity responseAsEntity =
    restTemplate.execute(urlToCall, HttpMethod.GET, null, responseExtractor);

您的里程可能会有所不同。 ;-)


6
投票

这是一个简单的解决方案,您可以设置默认的 Content-Type 以供在响应中丢失时使用。 Content-Type 会添加到响应标头中,然后再将其交还给预配置的 ResponseExtractor 进行提取。

public class CustomRestTemplate extends RestTemplate {

    private MediaType defaultResponseContentType;

    public CustomRestTemplate() {
        super();
    }

    public CustomRestTemplate(ClientHttpRequestFactory requestFactory) {
        super(requestFactory);
    }

    public void setDefaultResponseContentType(String defaultResponseContentType) {
        this.defaultResponseContentType = MediaType.parseMediaType(defaultResponseContentType);
    }

    @Override
    protected <T> T doExecute(URI url, HttpMethod method, RequestCallback requestCallback, final ResponseExtractor<T> responseExtractor)
            throws RestClientException {

        return super.doExecute(url, method, requestCallback, new ResponseExtractor<T>() {
            public T extractData(ClientHttpResponse response) throws IOException {
                if (response.getHeaders().getContentType() == null && defaultResponseContentType != null) {
                    response.getHeaders().setContentType(defaultResponseContentType);
                }

                return responseExtractor.extractData(response);
            }
        });
    }
}

4
投票

现在应该在 Spring 3.1 RC1 中修复此问题。

https://github.com/spring-projects/spring-framework/issues/12566(以前的SPR-7911)


2
投票

或者您可以扩展 RestTemplate 并覆盖 doExecute(..) 并检查响应正文。

例如,这是我实施并为我们工作的:

@Override
protected <T> T doExecute(final URI url, final HttpMethod method, final RequestCallback requestCallback, final ResponseExtractor<T> responseExtractor)
        throws RestClientException
{
    Assert.notNull(url, "'url' must not be null");
    Assert.notNull(method, "'method' must not be null");
    ClientHttpResponse response = null;
    try
    {
        final ClientHttpRequest request = createRequest(url, method);
        if (requestCallback != null)
        {
            requestCallback.doWithRequest(request);
        }
        response = request.execute();
        if (!getErrorHandler().hasError(response))
        {
            logResponseStatus(method, url, response);
        }
        else
        {
            handleResponseError(method, url, response);
        }
        if ((response.getBody() == null) || (responseExtractor == null))
        {
            return null;
        }
        return responseExtractor.extractData(response);
    }
    catch (final IOException ex)
    {
        throw new ResourceAccessException("I/O error: " + ex.getMessage(), ex);
    }
    finally
    {
        if (response != null)
        {
            response.close();
        }
    }
}

1
投票

我认为你是对的。 我也有类似的问题。 我认为我们应该得到一个 HttpStatus 为 NO_CONTENT 且主体为 null 的 ResponseEntity。


0
投票

我找到了一个解决方法(不确定它是否符合您的情况):

首先定义一个实现ClientHttpRequestInterceptor的自定义拦截器类。并检查response.getStatusCode()是否满足您的情况(我的情况是!= HttpStatus.NOT_FOUND并且response.getBody()长度为0),定义一个自定义类(例如DefaultResponseForEmptyRestTemplateBody),它具有类型为MockClientHttpResponse的静态方法

    public class RequestResponseInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {

    ClientHttpResponse response = execution.execute(request, body);
     if(response.getStatusCode()!=HttpStatus.NOT_FOUND && response.getBody().readAllBytes().length==0){
        response = DefaultResponseForEmptyRestTemplateBody.getResponse(response.getStatusCode());

    }

    return response;
}


}

    public static class DefaultResponseForEmptyRestTemplateBody  {
MockClientHttpResponse response;
private static byte[] content = new byte[0];

public static MockClientHttpResponse getResponse(HttpStatus statusCode){
    content = "response body is empty".getBytes();
  return new MockClientHttpResponse(content, statusCode);
}

}

最后将此拦截器添加到您的restTemplate对象中,如下所示:

restTemplate.setInterceptors(Collections.singletonList(new RequestResponseLoggingInterceptor()));

并调用你的restTemplate.postForEntity:

ResponseEntity<String> response = this.restTemplate.postForEntity(baseUrl, requestParams,String.class);
© www.soinside.com 2019 - 2024. All rights reserved.