Многие приложения должны представлять ошибки, которые происходят в приложении, своим пользователям в удобной для чтения форме. В этой статье рассматриваются некоторые широко используемые лучшие практики по этой теме.
Прежде чем мы начнем, давайте внесем ясность:
НИКОГДА не раскрывайте трассировки стека в ваших ответах API
Это не только некрасиво, но и опасно с точки зрения безопасности. Основываясь на номерах строк в этой трассировке стека, злоумышленник может определить библиотеки и версии, которые вы используете, и атаковать вас, используя их известные слабые места.
Тогда что показать нашим пользователям?
Назначение сообщения об исключении
Самая простая идея – отобразить строку сообщения об исключении для пользователей. Для простых приложений это может сработать, но в приложениях среднего размера возникают две проблемы:
Во-первых, вы можете захотеть выполнить модульный тест на то, что было выдано определенное исключение. Утверждение строк сообщений приведет к хрупким тестам, особенно когда вы начнете добавлять к этим сообщениям объединяющий пользовательский ввод:
throw new IllegalArgumentException("Invalid username: " + username);
Во-вторых, кто-то может возразить, что вы нарушаете Model-View-Controller , потому что вы смешиваете разные проблемы в одном и том же коде: форматирование видимого пользователем сообщения И обнаружение условия бизнес-ошибки.
Лучшая практика : Используйте сообщение об исключении, чтобы сообщить любые технические подробности об ошибке, которые могут быть полезны разработчикам, исследующим исключение.
Вы можете зайти так далеко, чтобы перехватить исключение в методе x()
и повторно отправить его обратно, завернутое в другое исключение, с полезным сообщением, которое фиксирует ключевую отладочную информацию из этого метода x()
. Например, вы можете включить аргументы метода, любой интересный идентификатор, индекс текущей строки и любую другую полезную отладочную информацию. Это шаблон Catch-rethrow-with-debug-message :
throw new RuntimeException("For id: " + keyDebugId, e);
Вы даже можете создать экземпляр и отбросить тот же тип исключения, который вы перехватили ранее. Я в полном порядке, пока вы сохраняете исходное исключение в цепочке, передавая его новому конструктору исключения в качестве его причины
.
Использование этого шаблона включит в вашу окончательную трассировку стека исключений всю необходимую вам полезную отладочную информацию, что позволит вам избежать отладки с точки останова. 👍 Я объясню больше советов о том, как избежать точки останова, в следующем сообщении в блоге.
С другой стороны, пожалуйста, не пишите фразы из более чем 30 слов в своих сообщениях об исключениях. Они просто отвлекут читателя: его мозг будет бесконечно пялиться на эту милую, понятную для человека фразу в середине этого сложного кода. И, честно говоря, сообщения об исключениях – это наименее важные вещи, которые следует читать при просмотре неизвестного кода. Имейте в виду, что первое, что делает разработчик при отладке исключения, – это щелкает по трассировке стека. Так что будь проще (ПОЦЕЛУЙ).
Наилучшая практика Сохраняйте свои сообщения об исключениях краткими и содержательными. Так же, как комментарии.
Подводя итог, я буду рекомендовать, чтобы в большинстве случаев вы сохраняли сообщение об исключении для краткой отладочной информации только для разработчиков. Сообщения об ошибках пользователя должны быть выведены наружу в .properties
files, чтобы вы могли легко настраивать, исправлять и использовать акценты UTF-8 без каких-либо проблем.
Но тогда как различать различные причины ошибок? Другими словами, каким должен быть ключ перевода в этом файле свойств?
Различные Подтипы Исключений
Может возникнуть соблазн начать создавать несколько подтипов исключений, по одному для каждой бизнес-ошибки нашего приложения, а затем различать ошибки на основе типа исключения. Хотя поначалу это кажется забавным, это быстро приведет к созданию сотен классов исключений в любых типичных приложениях реального мира. Очевидно, что это неразумно.
Наилучшая практика : создавать только новый тип исключения E1
если вы используете need to catch(E1)
и выборочно обрабатывать этот конкретный тип исключения, чтобы обойти его или восстановиться после него.
Но в большинстве приложений вы почти никогда не обходите исключения и не восстанавливаетесь после них. Вместо этого вы обычно завершаете выполнение текущего варианта использования/запроса, позволяя исключению всплывать на внешние уровни.
Перечисление Кода ошибки
Лучшим решением для различения ваших невосстановимых состояний ошибки является использование кода ошибки, а не сообщения об исключении или типа исключения.
Должен ли этот код ошибки быть интересным
? Если бы это было так, нам понадобилось бы Руководство по исключениям под рукой каждый раз, когда мы просматриваем код. Ужасный сценарий! Но подождите!
Каждый раз, когда в Java диапазон значений конечен и заранее известен, мы всегда должны рассматривать возможность использования enum
. Давайте дадим ему первую попытку:
public class MyException extends RuntimeException { public enum ErrorCode { GENERAL, BAD_CONFIG; } private final ErrorCode code; public MyException(ErrorCode code, Throwable cause) { super(cause); this.code = code; } public ErrorCode getCode() { return code; } }
И затем каждый раз, когда мы сталкиваемся с проблемой из-за плохой конфигурации, мы могли бы выполнить throw new MyException(Код ошибки. BAD_CONFIG, e);
.
Модульные тесты также более надежны при использовании enum
для кодов ошибок по сравнению с сопоставлением строкового сообщения:
// junit 4.13 or 5: MyException e = assertThrows(MyException.class, () -> method(...)); assertThat(ErrorCode.BAD_CONFIG, e.getErrorCode());
Хорошо, мы разобрались, как различать ошибки, и мы упомянули, что сообщения об исключениях пользователя должны быть экстернализованы в .properties
files. Но кто и когда делает этот перевод? Давайте посмотрим…
Глобальный Обработчик Исключений
Давайте напишем обработчик GlobalExceptionHandler, который перехватит наше исключение и преобразует его в дружественное сообщение для наших пользователей. Пользователь обычно отправляет HTTP-запросы, поэтому мы предположим, что конечная точка REST в приложении Spring Boot, но все другие основные веб-фреймворки сегодня предлагают аналогичную функциональность.
@RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); private final MessageSource messageSource; public GlobalExceptionHandler(MessageSource messageSource) { this.messageSource = messageSource; } @ExceptionHandler(MyException.class) @ResponseStatus // by default returns 500 error code public String handleMyException(MyException exception, HttpServletRequest request) { String userMessage = messageSource.getMessage(exception.getCode().name(), null, request.getLocale()); log.error(userMessage, exception); return userMessage; } }
Фреймворк перейдет к этому @RestControllerAdvice
каждое MyException
, которое ускользает от любой конечной точки REST ( @RequestMapping
, @GetMapping
…).
На этом этапе иногда возникают споры: куда мы должны перевести код ошибки: на стороне сервера (на Java) или в браузере (на JavaScript)?
Хотя заманчиво перенести проблему на внешний интерфейс, перевод кодов ошибок на серверную часть, как правило, лучше локализует влияние добавления нового кода ошибки. Действительно, почему внешний код должен меняться при добавлении или удалении новой ошибки?
Кроме того, вы можете даже написать код, чтобы проверить при запуске приложения, что все коды ошибок перечисления имеют перевод в файлах .properties
. Вот почему я переведу ошибку на серверную часть и отправлю удобные для пользователя сообщения в HTTP-ответах.
Мои предпочтения : перевести коды ошибок на серверной части
Я использовал Spring MessageSource
для чтения пользовательского сообщения, соответствующего коду ошибки пойманного MyException
. В зависимости от языкового стандарта пользователя Источник сообщения
определяет, из какого файла следует читать, например: messages_RO.properties
для RO , messages_FR.properties
для бесплатно или по умолчанию из messages.properties
для любого другого языка.
Я извлек языковой стандарт пользователя из заголовка Accept-Language в HTTP-запросе. Однако во многих приложениях пользовательский язык считывается из профиля пользователя, а не из входящего HTTP-запроса.
В src/main/resources/messages.properties
мы храним переводы:
BAD_CONFIG=Incorrect application configuration
Вот и все! Бесплатная интернационализация для наших сообщений об ошибках.
Однако осталась небольшая проблема. Что, если мы хотим сообщить о каком-либо пользовательском вводе, вызывающем ошибку, обратно в пользовательский интерфейс? Нам нужно добавить параметры в MyException
. После добавления других удобных конструкторов, вот его полный код:
public class MyException extends RuntimeException { public enum ErrorCode { GENERAL, BAD_CONFIG; } private final ErrorCode code; private final Object[] params; // canonical constructor public MyException(String message, ErrorCode code, Throwable cause, Object... params) { super(message, cause); this.code = code; this.params = params; } // 6 more overloaded constructors for convenience public ErrorCode getCode() { return code; } public Object[] getParams() { return params; } }
Это позволит нам использовать эти параметры в файлах messages*.properties
:
BAD_CONFIG=Incorrect application configuration. User info: {0}
О, и мы должны передать эти аргументы источнику Сообщения
когда мы запрашиваем перевод:
String userMessage = messageSource.getMessage(exception.getCode().name(), exception.getParams(), request.getLocale());
Чтобы увидеть, как работает все приложение, перейдите по ссылке этот коммит , запустите класс Spring Boot
и перейдите к http://localhost:8080 .
Сообщение об Общих ошибках
Когда мы должны использовать код ошибки GENERAL
?
Когда дело доходит до того, какие сообщения об ошибках отображать пользователям, многие хорошие разработчики испытывают соблазн различать столько ошибок |/, сколько они могут обнаружить. Другими словами, они будут считать, что их пользователи должны получать уведомления о каждой причине сбоя. Хотя ставить себя на место своих пользователей обычно является хорошей практикой, в данном случае это ловушка.
Как правило, вы должны предпочесть отображение непрозрачного сообщения об ошибке общего назначения, например “Внутренняя ошибка сервера, пожалуйста, проверьте журналы”. Внимательно подумайте, может ли пользователь ~ разработчик ~ что-либо сделать с этой ошибкой или нет. Если нет, не утруждайте себя определением этой конкретной причины ошибки, потому что, если вы это сделаете, вам придется убедиться, что вы продолжаете обнаруживать that с этого момента причина ошибки. Сегодня это может быть проще, но кто знает через 3 года… Это хороший пример ненужных и непредвиденных долгосрочных затрат на техническое обслуживание.
Лучшая практика : Избегайте отображения точной причины ошибки вашим пользователям, если они не могут что-то сделать, чтобы исправить ее.
Честно говоря, если конфигурация приложения повреждена, может ли пользователь действительно это исправить? Вероятно, нет. Поэтому мы должны использовать new MyException(Код ошибки. ОБЩИЕ СВЕДЕНИЯ)
или другая удобная перегрузка конструктора new MyException()
, которая делает то же самое.
Мы можем упростить это еще больше и напрямую вызвать новое исключение RuntimeException("сообщение отладки?"...)
. Сонар, возможно, не очень доволен этим, но это самая простая из возможных форм.
Но что, если нет дополнительного отладочного сообщения для добавления к новому исключению? Тогда ваш код может выглядеть следующим образом:
} catch (SomeException e) throw new RuntimeException(e); }
Обратите внимание, что я не регистрировал исключение перед повторным выбросом. Это на самом деле является анти-шаблоном, как я объясню в следующем сообщении в блоге.
Но посмотрите еще раз на приведенный выше фрагмент кода. С какой стати вам ловить это Какое-то исключение
? Если бы это было исключение во время выполнения, почему бы вам не позволить ему пролететь? Итак Некоторое исключение
должно быть проверенным исключением, и вы делаете это, чтобы преобразовать его в невидимое во время выполнения исключение. Если это так, то у вас есть еще один широко используемый вариант: добавьте аннотацию @SneakyThrows
из Lombok к этому методу, чтобы эффективно позволить проверенному исключению незаметно распространяться по стеку вызовов. Вы можете прочитать больше об этой технике в моем другом сообщении в блоге .
И последнее: нам нужно улучшить наш глобальный обработчик исключений, чтобы он также перехватывал любые другие Исключение
, которое ускользает от RestController
метод, подобный печально известному NullPointerException
. Нам нужно добавить еще один метод:
@ExceptionHandler(Exception.class) @ResponseStatus public String handleAnyOtherException(Exception exception, HttpServletRequest request) { String userMessage = messageSource.getMessage(ErrorCode.GENERAL.name(), null, request.getLocale()); log.error(userMessage, exception); return userMessage; }
Обратите внимание, что мы использовали код, аналогичный предыдущему методу @ExceptionHandler
для чтения сообщений об ошибках пользователя из тех же файлов .properties
. Вы можете сделать немного больше полировки, но это основные моменты.
Выводы
- Используйте краткие сообщения об исключениях для передачи разработчикам отладочной информации
- Catch-rethrow-with-debug-message для отображения большего количества данных в трассировке стека
- Определяйте новые типы исключений только в том случае, если вы их выборочно перехватываете
- Используйте перечисление кода ошибки для перевода ошибок пользователям
- При необходимости добавьте неверный пользовательский ввод в параметры исключения и оформите свои сообщения об ошибках
- Мы внедрили глобальный обработчик исключений в Spring для перехвата, регистрации и трансляции всех исключений, полученных из HTTP-запросов (+ интернационализированных)
Оригинал: “https://dev.to/victorrentea/presenting-exceptions-to-users-1pac”