Как мне восстановиться после непроверенного исключения?

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

Но я хочу оправиться от конкретных проблем, и я не уверен, что лучший способ подойти к нему с непроверенными исключениями. Вот конкретный пример.

Предположим, у меня есть веб-приложение, созданное с использованием Struts2 и Hibernate. Если из-за моего "действия" вспыхивает исключение, я записываю его в журнал и извиняюсь перед пользователем. Но одна из функций моего веб-приложения - создание новых учетных записей, для которых требуется уникальное имя пользователя. Если пользователь выбирает имя, которое уже существует, Hibernate выбрасывает org.hibernate.exception.ConstraintViolationException (непроверенное исключение) в глубине души моей системы. Мне бы очень хотелось избавиться от этой конкретной проблемы, попросив пользователя выбрать другое имя пользователя, вместо того, чтобы дать им то же сообщение "мы зарегистрировали вашу проблему, но пока вы попались".

Вот несколько моментов для рассмотрения:

  1. Там много людей, создающих аккаунты одновременно. Я не хочу блокировать всю пользовательскую таблицу между "SELECT", чтобы увидеть, существует ли имя, и "INSERT", если его нет. В случае реляционных баз данных могут быть некоторые уловки, чтобы обойти это, но что меня действительно интересует, так это общий случай, когда предварительная проверка на исключение не будет работать из-за фундаментального состояния гонки. То же самое может относиться к поиску файла в файловой системе и т. Д.
  2. Учитывая склонность моего технического директора к управлению проездом, вызванную чтением технологических колонок в "Инк.", Мне нужен слой косвенности вокруг механизма персистентности, чтобы я мог выбросить Hibernate и использовать Kodo, или что угодно, не меняя ничего, кроме самого низкого слой персистентного кода. На самом деле, в моей системе есть несколько таких уровней абстракции. Как я могу предотвратить их утечку, несмотря на непроверенные исключения?
  3. Одним из заявленных недостатков проверенных исключений является необходимость "обрабатывать" их при каждом вызове в стеке - либо объявив, что вызывающий метод их выбрасывает, либо перехватив их и обработав их. Обработка их часто означает включение их в другое проверенное исключение типа, соответствующего уровню абстракции. Так, например, на земле с проверенными исключениями реализация моего UserRegistry на основе файловой системы может поймать IOExceptionв то время как реализация базы данных будет ловить SQLException, но оба бросили UserNotFoundException это скрывает основную реализацию. Как мне воспользоваться неконтролируемыми исключениями, избавляя себя от бремени этого переноса на каждом уровне, не пропуская детали реализации?

9 ответов

Решение

IMO, упаковка исключений (проверено или нет) имеет несколько преимуществ, которые стоят затрат:

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

2) Это дает вам возможность добавить дополнительную отладочную информацию в цепочку исключений. Например, если у вас есть метод, который выбрасывает исключение для дублированного имени пользователя, вы можете заключить это исключение в одно, включающее дополнительную информацию об обстоятельствах сбоя (например, IP-адрес запроса, предоставившего двойное имя пользователя), который не был доступен для кода нижнего уровня. Файл cookie с исключениями может помочь вам отладить сложную проблему (это, безусловно, имеет для меня).

3) Это позволяет вам стать независимым от реализации кода нижнего уровня. Если вы включаете исключения и вам нужно заменить Hibernate на какой-либо другой ORM, вам нужно только изменить код обработки Hibernate. Все остальные уровни кода будут по-прежнему успешно использовать обернутые исключения и будут интерпретировать их таким же образом, даже если основные обстоятельства изменились. Обратите внимание, что это применимо, даже если Hibernate каким-либо образом изменяется (например: они переключают исключения в новой версии); это не только для замены технологий оптом.

4) Рекомендуется использовать разные классы исключений для представления разных ситуаций. Например, у вас может быть исключение DuplicateUsernameException, когда пользователь пытается повторно использовать имя пользователя, и исключение DatabaseFailureException, когда вы не можете проверить наличие дублированных имен пользователей из-за разорванного соединения с БД. Это, в свою очередь, позволяет вам гибко и эффективно ответить на ваш вопрос ("как мне восстановиться?"). Если вы получили исключение DuplicateUsernameException, вы можете решить предложить другое имя пользователя. Если вы получаете исключение DatabaseFailureException, вы можете позволить ему всплывать до того момента, когда он отображает пользователю страницу "не работает", и отправляет вам уведомление по электронной почте. Если у вас есть пользовательские исключения, у вас есть настраиваемые ответы - и это хорошо.

Мне нравится перепаковывать исключения между "уровнями" моего приложения, поэтому, например, исключение, специфичное для БД, переупаковывается в другое исключение, которое имеет смысл в контексте моего приложения (конечно, я оставляю исходное исключение в качестве члена, так что Я не забиваю след стека).

Тем не менее, я думаю, что неуникальное имя пользователя не является "исключительной" ситуацией, достаточной для оправдания броска. Вместо этого я бы использовал логический аргумент возврата. Не зная о вашей архитектуре, мне трудно сказать что-то более конкретное или применимое.

См. Шаблоны для генерации, обработки и управления ошибками

Из шаблона "Разделение домена и технические ошибки"

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

Ошибки домена всегда должны начинаться с проблемы домена и обрабатываться кодом домена.

Ошибки домена должны проходить "плавно" через технические границы. Может случиться так, что такие ошибки должны быть сериализованы и восстановлены, чтобы это произошло. Прокси и фасады должны взять на себя ответственность за это.

Технические ошибки следует обрабатывать в определенных точках приложения, таких как границы (см. Журнал на границе распределения).

Объем контекстной информации, передаваемой обратно с ошибкой, будет зависеть от того, насколько она будет полезна для последующей диагностики и обработки (выяснения альтернативной стратегии). Вам необходимо задать вопрос, является ли трассировка стека с удаленного компьютера полностью полезной для обработки ошибки домена (хотя расположение кода ошибки и значений переменных в то время может быть полезным)

Таким образом, оберните исключение hibernate на границе в состояние hibernate с исключением непроверенного домена, таким как "UniqueUsernameException", и дайте этому пузырю всплыть вплоть до его обработчика. Убедитесь, что Javadoc выброшенное исключение, хотя это не проверенное исключение!

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

Также я бы порекомендовал взглянуть на документацию по блоку обработки исключений Microsoft Enterprise Library, в котором есть хорошая схема шаблонов обработки ошибок.

  1. Вопрос на самом деле не связан с дебатами с проверкой и без проверки, то же самое относится к обоим типам исключений.

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

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

  4. Если мы хотим обработать "нарушение уникального имени" только путем показа пользователю красивого сообщения об ошибке (страница ошибки), на самом деле не нужно особого исключения DuplicateUsernameException. Это снизит количество классов исключений. Вместо этого мы можем создать исключение MessageException, которое можно использовать во многих похожих сценариях.

    Как можно скорее мы перехватываем ConstraintViolationException и преобразуем его в MessageException с хорошим сообщением. Очень важно преобразовать его в ближайшее время, когда мы можем быть уверены, что это было действительно "ограничение уникального имени пользователя", а не какое-то другое ограничение.

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

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

Код может выглядеть так

// insert the user
try {
   hibernateSession.save(user);
} catch (ConstraintViolationException e) {
   throw new MessageException("Username " + user.getName() + " already exists. Please choose a different name.");
}

В совершенно другом месте есть главный обработчик исключений

try {
   ... render the page
} catch (MessageException e) {
   ... render a nice page with the message
} catch (Exception e) {
   ... render "we logged your problem but for now you're hosed" message
}

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

Помогите?

@Jan Проверено, а не проверено - центральная проблема здесь. Я подвергаю сомнению ваше предположение (#3), что исключение следует игнорировать во промежуточных кадрах. Если я сделаю это, в моем высокоуровневом коде появится зависимая от реализации зависимость. Если я заменю Hibernate, блоки catch во всем приложении придется модифицировать. Тем не менее, в то же время, если я поймаю исключение на более низком уровне, я не получу особой выгоды от использования неконтролируемого исключения.

Also, the scenario here is that I want to catch a specific logical error and change the flow of the application by re-prompting the user for a different ID. Simply changing the displayed message is not good enough, and the ability to map to different messages based on exception type is built into Servlets already.

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

try {
    throw new IllegalArgumentException();
} catch (Exception e) {
    System.out.println("boom");
}

Таким образом, в вашем действии / контроллере вы можете иметь блок try-catch вокруг логики, в которой выполняется вызов Hibernate. В зависимости от исключения вы можете отображать конкретные сообщения об ошибках.

Но я думаю, что сегодня у вас может быть Hibernate, а завтра SleepLongerDuringWinter Framework. В этом случае вам нужно притвориться, что у вас есть собственная небольшая платформа ORM, которая охватывает стороннюю платформу. Это позволит вам превратить любые специфичные для фреймворка исключения в более значимые и / или проверенные исключения, которые вы знаете, как лучше понять.

@erikson

Просто чтобы добавить еду в ваши мысли:

Проверено и не проверено также обсуждается здесь

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

Что касается вашей конкретной проблемы, вы должны перехватить непроверенное исключение на высоком уровне и инкапсулировать его, как говорит @Kanook в вашем собственном исключении, без отображения стека вызовов (как упомянуто @Jan Soltis)

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

Другие вопросы по тегам