Подтвердить что ты не робот

Обработка ошибок в Java SDK для службы REST API

Мы строим Java SDK, чтобы упростить доступ к одной из наших сервисов, которые предоставляют REST API. Этот SDK будет использоваться сторонними разработчиками. Я изо всех сил пытаюсь найти лучший образец для реализации обработки ошибок в SDK, который лучше подходит для языка Java.

Скажем, у нас есть остальная конечная точка: GET /photos/{photoId}. Это может вернуть следующие коды состояния HTTP:

  • 401: Пользователь не аутентифицирован
  • 403: у пользователя нет разрешения на доступ к этой фотографии.
  • 404: Нет фотографии с этим id

Служба выглядит примерно так:

interface RestService {   
    public Photo getPhoto(String photoID);
} 

В приведенном выше коде я пока не обращаюсь к обработке ошибок. Я, очевидно, хочу предоставить способ для клиента sdk узнать, какая ошибка произошла, чтобы потенциально восстановить его. Обработка ошибок в Java выполняется с использованием Exceptions, поэтому отпустите это. Однако, каков наилучший способ сделать это, используя исключения?

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) + перечислите их в сигнатуре метода

Итак, как насчет исключений Unchecked? Если мы изменим исключение RestServiceException, чтобы расширить исключение RuntimeException:

public class RestServiceException extends RuntimeException

И мы сохраняем подпись метода:

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

Таким образом, я могу добавить новые исключения к сигнатуре метода, не нарушая существующий код. Тем не менее, с помощью этого решения разработчик не вынужден улавливать какое-либо исключение и не замечает, что ему приходится обрабатывать ошибки, пока он внимательно не прочитает документацию (да, правильно!) Или не заметил Исключения, которые находятся в подписи метода.

Какая наилучшая практика для обработки ошибок в таких ситуациях?

Есть ли другие (лучшие) альтернативы тем, о которых я говорил?

4b9b3361

Ответ 1

Я видел библиотеки, которые объединяют ваши предложения 2 и 3, например

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

Таким образом, когда вы добавляете новое проверенное исключение, которое расширяет RestServiceException, вы не меняете контракт метода, и любой код, который он использует, все еще компилируется.

По сравнению с решением обратного вызова или неконтролируемого исключения преимущество заключается в том, что это гарантирует, что ваша новая ошибка будет обрабатываться кодом клиента, даже если это будет только как общая ошибка. В обратном вызове ничего не произойдет, и с неконтролируемым исключением ваше клиентское приложение может потерпеть крах.

Ответ 2

Альтернативы обработки исключений: Обратные вызовы

Я не знаю, является ли это лучшей альтернативой, но вы можете использовать обратные вызовы. Вы можете сделать некоторые методы необязательными, предоставив реализацию по умолчанию. Взгляните на это:

    /**
     * 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, поместите 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. Это всего лишь один простой пример: существует множество тем для обсуждения, когда вы создаете собственный клиент для отдыха. Например, "какая библиотека json используется для разбора?", "Работаете ли вы на Android или на Java?" (это важно, потому что я не знаю, поддерживает ли андроид некоторые функции java 7, такие как исключения с несколькими catch, и есть некоторые технологии, которые недоступны для java или android и наоборот).

Но самое лучшее, что я могу сказать, это код sdk api с точки зрения пользователя, обратите внимание, что строк для остального запроса немного.

Надеюсь, это поможет! Пока:]

Ответ 3

Кажется, вы делаете вещи "рукой". Я бы рекомендовал вам попробовать Apache CXF.

Это аккуратная реализация API JAX-RS, которая позволяет вам почти забыть о REST. Он хорошо работает (рекомендуется также) Spring.

Вы просто пишете классы, которые реализуют ваши интерфейсы (API). Что вам нужно сделать, это аннотировать методы и параметры ваших интерфейсов с аннотациями JAX-RS.

Затем CXF делает магию.

Вы выбрали нормальные исключения в своем java-коде, а затем используйте команду отображения исключений на сервере /nd или клиенте для перевода между ними и кодом состояния HTTP.

Таким образом, на стороне сервера/клиента Java вы имеете дело только с обычным исключением на 100% Java, а CXF обрабатывает HTTP для вас: у вас есть как преимущества ясного API REST, так и клиента Java, готового для использования ваших пользователей.

Клиент может быть сгенерирован из вашего WDSL или создан во время выполнения из интроспекции аннотаций интерфейса.

Смотрите:

В нашем приложении мы определили и отобразили набор кодов ошибок и их экземпляр Исключения:

  • 4XX Ожидаемое/функциональное исключение (например, плохие аргументы, пустые наборы и т.д.)
  • 5XX Неожиданное/Неустранимое RunTimeException для внутренних ошибок, которые "не должны произойти"

Он следует как стандартам REST, так и Java.

Ответ 4

Решение может отличаться в зависимости от ваших потребностей.

  • Если предполагается, что в будущем могут возникнуть непредсказуемые новые типы исключений, лучшим решением будет ваше второе решение с проверенной иерархией исключений и методом, которые бросают их суперкласс 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 exeption для использования в бизнес-логике "все или ничего".

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

Ответ 5

Все сводится к тому, насколько информативны ваши ответы об ошибках API. Чем более информативна обработка ошибок API, тем более информативной может быть обработка исключений. Я бы поверила, что исключения будут только информативными, как ошибки, возвращенные API.

Пример:

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

Следуя вашему первому предложению, если в Исключении содержится как статус, так и код ошибки API, разработчик лучше понимает, что ему нужно делать, и больше вариантов, когда дело касается обработки исключений. Если в исключении также содержится сообщение об ошибке, которое было возвращено, разработчику также не нужно ссылаться на документацию.