21

我们正在构建一个 Java SDK 来简化对我们提供 REST API 的服务之一的访问。此 SDK 供第 3 方开发人员使用。我正在努力寻找在 SDK 中实现更适合 Java 语言的错误处理的最佳模式。

假设我们有其余端点: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;

但是,将来可能会将新的错误情况添加到此休息端点。这意味着在方法的签名中添加一个新的异常,这将是对 java api 的重大更改。我们希望有一个更强大的解决方案,不会导致在所描述的情况下对 api 进行重大更改。

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

那么,Unchecked 异常呢?如果我们更改 RestServiceException 以扩展 RuntimeException:

public class RestServiceException extends RuntimeException

我们保留方法的签名:

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

这样,我可以在不破坏现有代码的情况下向方法的签名添加新的异常。但是,使用此解决方案,开发人员不会被迫捕获任何异常,并且在他仔细阅读文档(是的,正确的!)或注意到方法签名中的异常之前,不会注意到他需要处理的错误情况.

在这种情况下,错误处理的最佳做法是什么?

除了我提到的那些,还有其他(更好的)替代品吗?

4

5 回答 5

16

异常处理替代方案:回调

我不知道这是否是一个更好的选择,但你可以使用回调。您可以通过提供默认实现来使某些方法成为可选方法。看看这个:

    /**
     * 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 中一样。

如何明确定义异常处理的内容?

如果您问我,最好的方法是绘制请求的生命周期,如下所示:

示例异常生命周期

这只是一个糟糕的例子,但重要的是要记住所有方法的实现,可能是也可能不是,可选的。如果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 进行编码,请注意发出其余请求的行很少。

希望这可以帮助!再见 :]

于 2013-10-16T00:52:37.267 回答
10

看来您是在“手工”做事。我建议你尝试一下Apache CXF

这是JAX-RS API的简洁实现,使您几乎可以忘记 REST。它与(也推荐)Spring配合得很好。

您只需编写实现您的接口 (API) 的类。您需要做的是使用 JAX-RS 注释对接口的方法和参数进行注释。

然后,CXF 施展魔法。

您在 java 代码中抛出正常异常,然后在服务器/nd 或客户端上使用异常映射器在它们和 HTTP 状态代码之间进行转换。

这样,在服务器/Java 客户端,您只处理常规的 100% Java 异常,而 CXF 为您处理 HTTP:您既拥有清晰的 REST API 的好处,又拥有可供用户使用的 Java 客户端。

客户端可以从您的 WDSL 生成,也可以在运行时从接口注释的自省中构建。

看 :

  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 预期/功能异常(如错误参数、空集等)
  • 5XX Unexpected / Unrecovable RunTimeException 用于“不应该发生”的内部错误

它遵循 REST 和 Java 标准。

于 2013-10-15T22:52:21.767 回答
2

我见过结合了您的建议 2 和 3 的库,例如

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

这样,当您添加一个新的扩展检查异常时RestServiceException,您不会更改方法的合同,并且使用它的任何代码仍然可以编译。

与回调或未经检查的异常解决方案相比,一个优点是可以确保您的新错误将由客户端代码处理,即使它只是一般错误。在回调中,什么都不会发生,如果出现未经检查的异常,您的客户端应用程序可能会崩溃。

于 2013-11-05T04:44:37.403 回答
0

解决方案可能会因您的需要而异。

  • 如果假设将来可能会出现不可预测的新异常类型,那么您的第二种解决方案具有检查的异常层次结构和抛出其超类的方法RestServiceException是最好的解决方案。所有已知的子类都应该列在 javadocSubclasses: {@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;
        }
    }
    
  • 还有一个很好的做法是创建替代方法,该方法将抛出未经检查的异常,该异常将 RestServiceExceptionexeption 本身包装起来,以便在“全有或全无”业务逻辑中使用。

    public Photo getPhotoUnchecked(String photoID) {
        try {
            return getPhoto(photoID);
        catch (RestServiceException ex) {
            throw new RestServiceUncheckedException(ex);
        }
    }
    
于 2013-10-16T07:09:26.030 回答
-1

这一切都取决于您的 API 错误响应的信息量。API 的错误处理信息越丰富,异常处理的信息就越丰富。我相信异常只会像从 API 返回的错误一样提供信息。

例子:

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

按照您的第一个建议,如果异常同时包含状态和 API 错误代码,则开发人员可以更好地了解他需要做什么,并且在异常处理方面有更多选择。如果异常还包含返回的错误消息,那么开发人员甚至不需要参考文档。

于 2013-10-11T14:24:44.620 回答