RestTemplate&ResponseErrorHandler:给定不确定的返回对象时处理错误的优雅方法

时间:2018-07-24 12:10:15

标签: java spring-boot error-handling resttemplate

使用RestTemplate,我正在查询远程API以返回预期类型(如果是HTTP 2xx)或APIError(如果是HTTP 4xx / 5xx)的对象。

由于响应对象不确定,因此我实现了自定义ResponseErrorHandler并覆盖了handleError(ClientHttpResponse clientHttpResponse),以便在发生APIError时提取它。到目前为止一切顺利:

@Component
public class RemoteAPI {

    public UserOrders getUserOrders(User user) {
        addAuthorizationHeader(httpHeaders, user.getAccessToken());
        HttpEntity<TokenRequest> request = new HttpEntity<>(HEADERS);
        return restTemplate.postForObject(CUSTOMER_ORDERS_URI, request, UserOrders.class);
    }

    private class APIResponseErrorHandler implements ResponseErrorHandler {
        @Override
        public void handleError(ClientHttpResponse response) {
            try {
                APIError apiError = new ObjectMapper().readValue(response.getBody(), APIError.class);
            } catch ...
        }
    }

    private void refreshAccessToken(User user) {
        addAuthorizationHeader(httpHeaders, user.getAccessSecret());
        HttpEntity<TokenRequest> request = new HttpEntity<>(HEADERS);
        user.setAccessToken(restTemplate.postForObject(TOKEN_REFRESH_URI, request, AccessToken.class));
    }
}

面临的挑战是getUserOrders()或类似的API调用有时会因“可恢复”错误而失败;例如,API访问令牌可能已过期。然后,我们应该在重新尝试refreshAccessToken()之前对getUserOrders()进行API调用。诸如此类的可恢复错误应向用户隐藏,直到多次发生相同的错误为止,此时,它们被视为不可恢复/严重。

由于没有自动恢复功能,任何“关键”错误(例如:第二次失败,完全身份验证失败或传输层失败)都应报告给用户。

要记住,直到运行时才知道返回的对象类型,最简单,最鲁棒的错误管理逻辑管理方式是什么?

选项1:将错误对象作为类变量,在每个API调用方法中使用try / catch:

@Component
public class RemoteAPI {
    private APIError apiError;

    private class APIResponseErrorHandler implements ResponseErrorHandler {
        @Override
        public void handleError(ClientHttpResponse response) {
            try {
                this.apiError = new ObjectMapper().readValue(response.getBody(), APIError.class);
            } catch ...
        }
    }

    public UserOrders getUserOrders(User user) {
        try {
            userOrders = restTemplate.postForObject(CUSTOMER_ORDERS_URI, request, UserOrders.class);
        } catch (RestClientException ex) {
            // Check this.apiError for type of error
            // Check how many times this API call has been attempted; compare against maximum
            // Try again, or report back as a failure
        }
        return userOrders;
    }
}

优点:明确说明最初使用哪种方法进行调用

缺点:将类变量用于瞬时值。每个调用API的方法都有很多样板代码。错误处理逻辑遍布多种方法。

选项2:用户对象作为类变量/ ResponseErrorHandler中的错误管理逻辑

@Component
public class RemoteAPI {
    private User user;

    private class APIResponseErrorHandler implements ResponseErrorHandler {

        @Override
        public void handleError(ClientHttpResponse response) {
            try {
            APIError apiError = new ObjectMapper().readValue(response.getBody(), APIError.class);
            // Check this.apiError for type of error
            // Check how many times this API call has been attempted; compare against maximum
            // Try again...
            getUserOrders();            
            ...or report back as a failure
        } catch ...
    }
}

优点:错误管理逻辑在一个地方。

缺点:现在,用户对象必须是一个类变量,并且必须进行适当处理,因为否则无法在ResponseErrorHandler中访问该用户对象,因此无法像以前一样将其传递给getUserOrders(User)。需要跟踪每种方法被调用了多少次。

选项3:RemoteAPI类之外的错误管理逻辑

优点:将错误处理与业务逻辑分开

缺点:API逻辑现在在另一个类中

谢谢您的建议。

1 个答案:

答案 0 :(得分:2)

回答我自己的问题:事实证明,问题本身存在谬论。

我之所以实现ResponseErrorHandler,是因为我认为我需要它来解析响应,即使该响应返回了HTTP错误代码也是如此。实际上,事实并非如此。

This answer演示了可以通过捕获HttpStatusCodeException或使用其他标准RestTemplate将响应解析为一个对象。这就不需要自定义ResponseErrorHandler,因此不需要返回模棱两可类型的对象。传递给错误的方法可以捕获HttpStatusCodeException,尝试刷新访问令牌,然后通过递归再次调用自身。需要一个计数器来防止无限递归,但是可以通过它而不是一个类变量来实现。

缺点是它仍然需要在类中散布错误管理逻辑以及大量样板代码,但是比其他选项要整洁。

public UserOrders getUserOrders(User user, Integer methodCallCount) {
    methodCallCount++;
    UserOrders userOrders;
    try {
        userOrders = restTemplate.postForObject(USER_ORDERS_URI, request, UserOrders.class);
    } catch (RestClientException ex) {
        APIError apiError = new ObjectMapper().readValue(response.getBody(), APIError.class);
        if (methodCallCount < MAX_METHOD_CALLS) {
            if (apiError.isType(ACCESS_TOKEN_EXPIRED)) {
                refreshVendorAccessTokenInfo(user);
                userOrders = getUserOrders(user, methodCallCount);
            }
        }
    }
    return userOrders;
}