【问题标题】:How to parse the response body in Java, when the HTTP request has return status 401当 HTTP 请求返回状态 401 时,如何在 Java 中解析响应正文
【发布时间】:2014-12-27 21:01:13
【问题描述】:

我正在使用 Spring 的 RestTemplate 和 Jackson 使用 RESTful JSON API。在某些情况下,我们可能会收到带有自定义 JSON 正文的 Status 401(未经授权)响应,该响应由 API 制造商定义,如下所示:

{
    "code": 123,
    "message": "Reason for the error"
}

我们需要解析正文,并在我们的业务逻辑中使用code 属性。

这是我们需要解析的错误响应 Java 对象:

public class CustomError {

    @JsonProperty
    private Integer code;
    @JsonProperty
    private String message;

    public Integer getCode() {
       return code;
    }
    public String getMessage() {
        return message;
    }
}

还有一个自定义错误处理程序来执行此操作:

public class CustomErrorHandler extends DefaultResponseErrorHandler {
    private RestTemplate restTemplate;
    private ObjectMapper objectMapper;
    private MappingJacksonHttpMessageConverter messageConverter;


    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        return super.hasError(response);
    }

    @Override
    public void handleError(final ClientHttpResponse response) throws IOException {

        try {
            CustomError error = 
                (CustomError) messageConverter.read(CustomError.class, response);
            throw new CustomErrorIOException(error, error.getMessage());
        } catch (Exception e) {
            // parsing failed, resort to default behavior
            super.handleError(response);
        }
    }
}

错误处理程序失败,在 try 块中出现 HttpMessageNotReadableException

“无法读取 JSON:由于服务器身份验证,无法重试,处于流模式”

这就是我发送请求的方式:

restTemplate.postForObject(url, pojoInstance, responseClass);

如果使用普通的旧客户端程序(如 Postman)执行相同的请求,则会收到预期的 JSON 响应。因此,我认为问题可能在于 Spring 的 ClientHttpResponse 实现在 401 状态的情况下以某种方式不允许访问响应正文。

真的可以解析响应体吗?

更新

根据我的调查,RestTemplate 类使用ClientHttpResponse,而ClientHttpResponse 又创建了一个提供输入流的sun.net.www.protocol.http.HttpURLConnection。它在那里,输入流被忽略并抛出IOException

由于服务器身份验证无法重试,在流模式下

所以,HttpURLConnection 的实现导致了这个问题。

是否有可能避免这个问题?也许我们应该使用一种替代实现,在出现错误状态代码的情况下不忽略响应主体?你能推荐任何替代品吗?

【问题讨论】:

    标签: java spring api jackson resttemplate


    【解决方案1】:

    在不需要自定义处理程序的情况下尝试以下方法。这个想法是从 HttpStatusCodeException 中获取作为字符串的响应,然后您可以将其转换为您的对象。对于转换,我使用了 Jackson 的 ObjectMapper:

            try {
    
                restTemplate.postForObject(url, pojoInstance, responseClass);
    
            } catch (HttpStatusCodeException e) {
    
                if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) {
    
                    String responseString = e.getResponseBodyAsString();
    
                    ObjectMapper mapper = new ObjectMapper();
    
                    CustomError result = mapper.readValue(responseString,
                            CustomError.class);
                }
            }
    

    更新: 使用不同的工厂也可能会有所帮助,因为默认工厂中存在与您的问题相关的错误(请参阅下面的评论):

    RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    

    【讨论】:

    • 我试过这个解决方案,但它似乎不起作用。异常实际上被抛出,但响应流不包含数据。如果我使用 POSTMAN 尝试相同的请求,则响应正文包含正文,所以这似乎又是一个 RestTemplate 问题。
    • 我发现了一个描述您在 jira.spring.io/browse/SPR-9999 中遇到的问题的错误。它是从去年开始的,但可能尚未修复。它建议使用不同的工厂,以便使用另一个客户端实现 (RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
    • @Marios,感谢您的努力。我之前使用过SimpleClientHttpConnectionFactory。使用HttpComponentsClientHttpRequestFactory 时,您的解决方案对我有用 - 也许上面提到的sun.net.www.protocol.http.HttpURLConnection 没有被它使用。
    • Ivaylo,确实是这样,SimpleClientHttpConnectionFactory 使用 java.net 实现 HTTP 请求,而 HttpComponentsClientHttpRequestFactory 在下面使用 Apache 的 HttpClient
    • 请注意:HttpStatusCodeException 仅为您提供围绕 HTTP 状态代码的语法糖,但您在 RestClientResponseException 中拥有所需的“getRawStatusCode()”和“getResponseBodyAsString()”。
    【解决方案2】:

    我是这样做的:

    @Component
    public class RestTemplateFactory {
    public enum Type {
        JSON, XML
    }
    
    public RestTemplate create(Type type) {
        RestTemplate restTemplate = new RestTemplate();
        if (type == Type.XML) {
            Jaxb2RootElementHttpMessageConverter jaxbMessageConverter = new Jaxb2RootElementHttpMessageConverter();
            jaxbMessageConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.TEXT_HTML, MediaType.APPLICATION_XML));
            restTemplate.setMessageConverters(Lists.newArrayList(jaxbMessageConverter));
        }
        restTemplate.setErrorHandler(new BpmRestErrorHandler(restTemplate.getMessageConverters()));
        return restTemplate;
    }
    
    public HttpHeaders contentHeaders(Type type) {
        HttpHeaders headers = new HttpHeaders();
        if (type == Type.XML) {
            headers.setContentType(MediaType.APPLICATION_XML);
            headers.setAccept(Collections.singletonList(MediaType.APPLICATION_XML));
        } else {
            headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
            headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
        }
        return HttpHeaders.readOnlyHttpHeaders(headers);
    }
    }
    

    和处理程序:

    public class BpmRestErrorHandler extends DefaultResponseErrorHandler {
    
    private final List<HttpMessageConverter<?>> messageConverters;
    
    public BpmRestErrorHandler(List<HttpMessageConverter<?>> messageConverters) {
        this.messageConverters = messageConverters;
    }
    
    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        for (HttpMessageConverter messageConverter : messageConverters) {
            if (messageConverter.canRead(RestRuntimeException.class, response.getHeaders().getContentType())) {
                RestRuntimeExceptionData exceptionData =
                        (RestRuntimeExceptionData)messageConverter.read(RestRuntimeException.class, response);
                throw new BpmRestException(exceptionData);
            }
        }
        super.handleError(response);
    }
    }
    

    RestRuntimeExceptionData 是我的自定义 WebFault 对象。 它重用了 RestTemplate 的 HttpConverters。

    【讨论】:

    • 感谢您的意见和详尽的回答:)。我注意到您实际上从未抛出 RestRuntimeException,这让我想知道使用简单的 POJO 保存错误数据是否更好。我担心的是,在构建时,异常对象会收集堆栈跟踪,这是不必要的减速。您已经在 BpmRestException 中拥有相同的堆栈跟踪
    • 其实这只是一个复杂的pojo。令人困惑的是,同意。真正的例外是 - BpmRestException
    • 是的,如果那是 POJO,那么一切都很好。我将重命名课程(至少在帖子中)。异常通常会暴露一个接受另一个异常(原因)的构造函数,并且可能会像我一样容易混淆。
    【解决方案3】:

    我只在映射器中使用了clientHttpResponseClientHttpResponse.getBody() 是类似于HttpStatusCodeException.getResponseBodyAsString 的流。

    @Component
    public class xhandler extends RestTemplateErrorHandler{
    
    
        @Override
        protected boolean handleServiceSpecificError(ClientHttpResponse response) {
            try {
                ObjectMapper mapper = new ObjectMapper();
                CustomError error =mapper.readValue(response.getBody(), CustomError.class);
                throw new RestClientException(“x Service returned an error response with error-code: "+response.getStatusCode().toString() + error.getErrorMessage());
            } catch (IOException e) {
                LOG.error(create(this.getClass(), "002"), “x Service returned an error, but the error response could not be parsed: {}", e.toString(), e);
            }
            return false;
        }
    
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-09-09
      • 2011-12-10
      • 2020-01-11
      • 1970-01-01
      相关资源
      最近更新 更多