Java SDK中REST API服务的错误处理

22
我们正在构建一个Java SDK,旨在简化访问我们提供的REST API之一的服务。这个SDK将由第三方开发人员使用。我正在努力寻找最佳模式来实现SDK中最适合Java语言的错误处理。
假设我们有rest端点:GET /photos/{photoId}。 这可能会返回以下HTTP状态码:
  • 401:用户未经过身份验证
  • 403:用户没有权限访问此照片
  • 404:没有该id的照片
服务看起来像这样:
interface RestService {   
    public Photo getPhoto(String photoID);
} 

在上面的代码中,我还没有处理错误处理。显然,我想为sdk的客户端提供一种方式来知道发生了哪个错误,以便有可能从中恢复。在Java中使用异常来处理错误,所以我们可以采用这种方式。但是,使用异常的最佳方法是什么?
1. 使用包含错误信息的单个异常。
public Photo getPhoto(String photoID) throws RestServiceException;

public class RestServiceException extends Exception {
    int statusCode;

    ...
}

SDK的客户端可以像这样做:
try {
    Photo photo = getPhoto("photo1");
}
catch(RestServiceException e) {
    swtich(e.getStatusCode()) {
        case 401 : handleUnauthenticated(); break;
        case 403 : handleUnauthorized(); break;
        case 404 : handleNotFound(); break;
    }
}

但我并不喜欢这个解决方案,主要有两个原因:

  • 通过查看方法签名,开发人员无法知道他可能需要处理什么样的错误情况。
  • 开发人员需要直接处理HTTP状态码,并且需要知道它们在此方法的上下文中的含义(显然如果它们被正确使用,很多时候含义是已知的,但并非总是如此)。

2. 使用错误类层次结构

方法签名保持不变:

public Photo getPhoto(String photoID) throws RestServiceException;

但现在我们为每个错误类型创建异常:
public class UnauthenticatedException extends RestServiceException;
public class UnauthorizedException extends RestServiceException;
public class NotFoundException extends RestServiceException;

现在,SDK的客户端可以像这样执行以下操作:
try {
    Photo photo = getPhoto("photo1");
}
catch(UnauthenticatedException e) {
    handleUnauthorized();
}
catch(UnauthorizedException e) {
    handleUnauthenticated();
}
catch(NotFoundException e) {
    handleNotFound();
}

采用这种方法,开发人员无需了解生成错误的HTTP状态代码,只需处理Java异常。另一个优点是开发人员可以仅捕获他想要处理的异常(与以前情况不同,在那种情况下,它只能捕获单个异常(RestServiceException),然后再决定是否希望处理它或不处理)。

然而,仍存在一个问题。通过查看方法签名,开发人员仍然无法了解可能需要处理的错误类型,因为我们在方法签名中只有超类。

3. 拥有错误的类层次结构+将它们列在方法的签名中

好吧,现在想到的是将方法的签名更改为:

public Photo getPhoto(String photoID) throws UnauthenticatedException, UnauthorizedException, NotFoundException;

然而,未来可能会为此rest端点添加新的错误情况。这意味着需要将新的异常添加到该方法的签名中,这将是对Java API的破坏性更改。我们希望有一个更健壮的解决方案,不会导致API在描述的情况下发生破坏性变化。

4.使用错误的类层次结构(使用未经检查的异常)+在方法的签名中列出它们

那么,未经检查的异常怎么样?如果我们将RestServiceException更改为扩展RuntimeException:

public class RestServiceException extends RuntimeException

同时我们保留方法的签名:

public Photo getPhoto(String photoID) throws UnauthenticatedException, UnauthorizedException, NotFoundException;

这样做可以在不破坏现有代码的情况下向方法签名添加新的异常。但是,采用此解决方案,开发人员不被强制捕获任何异常,也不会注意到需要处理的错误情况,直到他仔细阅读文档(是的,没错!)或注意到方法签名中的异常。

这种情况下的最佳错误处理实践是什么?

是否有其他(更好的)替代方案?


你是为 Android 还是所有 Java 应用程序制作这个 SDK? - 4gus71n
这个问题很有趣,但是很主观,不是吗?我想要同时点赞和投票关闭,但我只会做第一个。 - Miserable Variable
@MiserableVariable,我认为SO已经很明确了,“除了我提到的那些,还有其他更好的选择吗?”他没有说类似“我该如何在自己的http客户端实现中管理错误代码?”之类的话。我不知道,这是我的看法。此外,他显示出已经进行了研究的迹象。 - 4gus71n
astrinx,由于某些原因我完全错过了你的第一条评论 :/ 我正在为所有Java应用程序构建这个。 - Mario Duarte
5个回答

17

异常处理的替代方案:回调函数

我不知道这是否是更好的替代方案,但你可以使用回调函数。通过提供默认实现,您可以使一些方法变成可选项。看看下面的例子:

    /**
     * Example 1.
     * Some callbacks will be always executed even if they fail or 
     * not, all the request will finish.
     * */
    RestRequest request = RestRequest.get("http://myserver.com/photos/31", 
        Photo.class, new RestCallback(){

            //I know that this error could be triggered, so I override the method.
            @Override
            public void onUnauthorized() {
                //Handle this error, maybe pop up a login windows (?)
            }

            //I always must override this method.
            @Override
            public void onFinish () {
                //Do some UI updates...
            }

        }).send();

这是回调类的外观:

public abstract class RestCallback {

    public void onUnauthorized() {
        //Override this method is optional.
    }

    public abstract void onFinish(); //Override this method is obligatory.


    public void onError() {
        //Override this method is optional.
    }

    public void onBadParamsError() {
        //Override this method is optional.
    }

}

通过这样做,您可以定义请求的生命周期,并管理请求的每个状态。您可以选择实现或不实现某些方法。您可以获取一些常见错误,并给用户处理的机会,例如在 onError 中。
如何清楚地定义要处理的异常?
如果问我,最好的方法是绘制请求的生命周期,类似于这样:

Sample exception lifecicle

这只是一个简单的例子,但重要的是要记住所有方法的实现都是可选的。如果onAuthenticationError是必须的,那么onBadUsername就不一定需要了,反之亦然。这就是使回调如此灵活的关键点。
那么我该如何实现Http客户端呢?
嗯,我对Http客户端并不是很了解,我经常使用apache HttpClient,但是各种Http客户端之间没有太大差异,它们最多具有一些更多或更少的功能,但归根结底,它们都是相同的。只需选择Http方法、输入URL和参数,然后发送即可。在本例中,我将使用Apache HttpClient。
public class RestRequest {
    Gson gson = new Gson();

    public <T> T post(String url, Class<T> clazz,
            List<NameValuePair> parameters, RestCallback callback) {
        // Create a new HttpClient and Post Header
        HttpClient httpclient = new DefaultHttpClient();
        HttpPost httppost = new HttpPost(url);
        try {
            // Add your data
            httppost.setEntity(new UrlEncodedFormEntity(parameters));
            // Execute HTTP Post Request
            HttpResponse response = httpclient.execute(httppost);
            StringBuilder json = inputStreamToString(response.getEntity()
                    .getContent());
            T gsonObject = gson.fromJson(json.toString(), clazz);
            callback.onSuccess(); // Everything has gone OK
            return gsonObject;

        } catch (HttpResponseException e) {
            // Here are the http error codes!
            callback.onError();
            switch (e.getStatusCode()) {
            case 401:
                callback.onAuthorizationError();
                break;
            case 403:
                callback.onPermissionRefuse();
                break;
            case 404:
                callback.onNonExistingPhoto();
                break;
            }
            e.printStackTrace();
        } catch (ConnectTimeoutException e) {
            callback.onTimeOutError();
            e.printStackTrace();
        } catch (MalformedJsonException e) {
            callback.onMalformedJson();
        }
        return null;
    }

    // Fast Implementation
    private StringBuilder inputStreamToString(InputStream is)
            throws IOException {
        String line = "";
        StringBuilder total = new StringBuilder();

        // Wrap a BufferedReader around the InputStream
        BufferedReader rd = new BufferedReader(new InputStreamReader(is));

        // Read response until the end
        while ((line = rd.readLine()) != null) {
            total.append(line);
        }

        // Return full string
        return total;
    }

}

这是一个RestRequest的示例实现。这只是一个简单的例子,当你制作自己的rest客户端时,有很多话题需要讨论。例如,“使用什么样的json库进行解析?”,“你是为Android还是为Java工作?”(这很重要,因为我不知道Android是否支持Java 7的一些功能,比如多异常捕获,而且有些技术对Java或Android不可用,反之亦然)。
但我可以说的最好的建议是按照用户的需求编写sdk api,注意制作rest请求的代码行数很少。
希望这能帮到你!再见:]

这是一个很好的建议,但我没有将其标记为正确答案的原因是它涉及完全改变SDK处理错误的方式,与我们目前所做的不同。这是一个太大的变化。 - Mario Duarte

10

看起来你正在手动完成一些事情。

我建议你尝试一下Apache CXF

它是一个很好的实现了JAX-RS API的工具,让你几乎可以忘记REST。 它还能和(同样推荐的)Spring很好地配合使用。

你只需编写实现接口(API)的类,然后使用JAX-RS注解对接口的方法和参数进行注释即可。

然后,CXF就会自动处理好剩下的事情。

在Java代码中,你可以像平常一样抛出异常,而CXF会根据服务器端/客户端上的异常映射器将其转换为HTTP状态码。

这样,在服务器端/Java客户端上,你只需要处理正常的100% Java异常,而CXF会帮你处理HTTP:你既有一个清晰的REST API,又拥有一个可供用户使用的Java客户端。

客户端可以从WSDL生成,也可以通过对接口注解进行内省在运行时构建。

参见:

  1. http://cxf.apache.org/docs/jax-rs-basics.html#JAX-RSBasics-Exceptionhandling
  2. http://cxf.apache.org/docs/how-do-i-develop-a-client.html

在我们的应用程序中,我们已经定义和映射了一组错误代码及其对应的异常:

  • 4XX Expected / Functional excecption(如错误参数、空集等)
  • 5XX Unexpected / Unrecovable RunTimeException用于内部“不应该发生”的错误

它遵循REST和Java标准。


3
我见过一些库将您的建议2和3结合起来,例如:
public Photo getPhoto(String photoID) throws RestServiceException, UnauthenticatedException, UnauthorizedException, NotFoundException;

这样,当您添加一个新的已检查异常并扩展RestServiceException时,您不会改变方法的契约,任何使用它的代码仍然可以编译。
与回调或未经检查的异常解决方案相比,优点在于这确保您的新错误将由客户端代码处理,即使只是作为一般错误。在回调中,什么也不会发生,在未经检查的异常中,您的客户端应用程序可能会崩溃。

这实际上最终就是我们所采用的方式 ;) - Mario Duarte

0
解决方案可能因您的需求而异。
  • 如果未来可能出现不可预测的新异常类型,那么您的第二个解决方案,使用检查异常层次结构和抛出其超类RestServiceException的方法,是最好的选择。所有已知的子类应该在javadoc中列出,例如Subclasses: {@link UnauthenticatedException}, ...,以让开发人员知道可能隐藏的异常类型。需要注意的是,如果某个方法只能抛出此列表中的少数异常,它们应该在该方法的javadoc中使用@throws进行描述。

  • 当所有RestServiceException的出现都意味着任何一个子类都可能隐藏在其后面时,此解决方案也适用。但在这种情况下,如果RestServiceException的子类没有特定的字段和方法,则您的第一个解决方案更可取,但需要进行一些修改:

    public class RestServiceException extends Exception {
        private final Type type;
        public Type getType();
        ...
        public static enum Type {
            UNAUTHENTICATED,
            UNAUTHORISED,
            NOT_FOUND;
        }
    }
    
  • 还有一个很好的实践是创建替代方法,该方法将抛出包装RestServiceException异常本身的未经检查的异常,以供“全有或全无”的业务逻辑使用。

    public Photo getPhotoUnchecked(String photoID) {
        try {
            return getPhoto(photoID);
        catch (RestServiceException ex) {
            throw new RestServiceUncheckedException(ex);
        }
    }
    

-1

一切归结于您的 API 错误响应提供多少信息。API 的错误处理越详细,异常处理就能提供越详细的信息。我认为异常只能与 API 返回的错误一样详细。

示例:

{ "status":404,"code":2001,"message":"Photo could not be found."}

根据你的第一个建议,如果异常包含状态和API错误代码,开发人员将更好地了解他需要做什么,并在处理异常时拥有更多选项。如果异常还包含返回的错误消息,开发人员甚至不需要参考文档。

谢谢您的回答,但我认为它并没有真正回答我的问题。异常显然会包含有关抛出它的情况的信息(javadoc)。然而,问题仍然在于使用哪种类型的异常(已检查 vs 未检查)以及方法签名(超类RestServiceException还是更具体的异常)。 - Mario Duarte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接