Как можно передать полезную информацию о состоянии исключению в Java?
Сначала я заметил некоторую путаницу с моим вопросом. Я не спрашиваю о том, как настроить регистратор и как правильно использовать регистратор, а скорее о том, как собрать всю информацию, которая была бы зарегистрирована на более низком уровне регистрации, чем текущий уровень регистрации в сообщении об исключении.
Я заметил два шаблона в Java для регистрации информации, которая может быть полезна разработчику при возникновении исключения.
Следующая картина кажется очень распространенной. По сути, вы просто располагаете информацию журнала журналов в оперативном режиме по мере необходимости, чтобы при возникновении исключения у вас была трассировка журнала.
try {
String myValue = someObject.getValue();
logger.debug("Value: {}", myValue);
doSomething(myValue);
}
catch (BadThingsHappenException bthe) {
// consider this a RuntimeException wrapper class
throw new UnhandledException(bthe);
}
Недостаток описанного выше подхода заключается в том, что если вашим пользователям требуются относительно тихие журналы и им требуется высокий уровень надежности до такой степени, что они просто не могут "повторить попытку в режиме отладки", сообщение об исключении само по себе содержит недостаточно данных для полезно для разработчика.
Следующий шаблон, который я видел, пытается смягчить эту проблему, но кажется уродливым:
String myValue = null;
try {
myValue = someObject.getValue();
doSomething(myValue);
}
catch (BadThingsHappenException bthe) {
String pattern = "An error occurred when setting value. [value={}]";
// note that the format method below doesn't barf on nulls
String detail = MessageFormatter.format(pattern, myValue);
// consider this a RuntimeException wrapper class
throw new UnhandledException(detail, bthe);
}
Вышеприведенный шаблон, похоже, несколько решает проблему, однако я не уверен, что мне нравится объявлять так много переменных за пределами блока try. Особенно, когда мне приходится иметь дело с очень сложными состояниями.
Единственный другой подход, который я видел, - это использование карты для хранения пар ключ-значение, которые затем выводятся в сообщение об исключении. Я не уверен, что мне нравится такой подход, так как он, кажется, создает раздувание кода.
Есть ли какой-нибудь Java vodoo, которого мне не хватает? Как вы обрабатываете информацию о состоянии вашего исключения?
11 ответов
Мы стремимся создавать наши наиболее важные классы исключений для конкретных приложений, используя некоторые специальные конструкторы, некоторые константы и ResourceBundle.
Пример фрагмента:
public class MyException extends RuntimeException
{
private static final long serialVersionUID = 5224152764776895846L;
private static final ResourceBundle MESSAGES;
static
{
MESSAGES = ResourceBundle.getBundle("....MyExceptionMessages");
}
public static final String NO_CODE = "unknown";
public static final String PROBLEMCODEONE = "problemCodeOne";
public static final String PROBLEMCODETWO = "problemCodeTwo";
// ... some more self-descriptive problem code constants
private String errorCode = NO_CODE;
private Object[] parameters = null;
// Define some constructors
public MyException(String errorCode)
{
super();
this.errorCode = errorCode;
}
public MyException(String errorCode, Object[] parameters)
{
this.errorCode = errorCode;
this.parameters = parameters;
}
public MyException(String errorCode, Throwable cause)
{
super(cause);
this.errorCode = errorCode;
}
public MyException(String errorCode, Object[] parameters, Throwable cause)
{
super(cause);
this.errorCode = errorCode;
this.parameters = parameters;
}
@Override
public String getLocalizedMessage()
{
if (NO_CODE.equals(errorCode))
{
return super.getLocalizedMessage();
}
String msg = MESSAGES.getString(errorCode);
if(parameters == null)
{
return msg;
}
return MessageFormat.format(msg, parameters);
}
}
В файле свойств мы указываем описания исключений, например:
problemCodeOne=Simple exception message
problemCodeTwo=Parameterized exception message for {0} value
Используя этот подход
- Мы можем использовать вполне читаемые и понятные выражения throw (
throw new MyException(MyException.PROBLEMCODETWO, new Object[] {parameter}, bthe)
) - Сообщения об исключениях являются "централизованными", могут легко поддерживаться и "интернационализироваться"
РЕДАКТИРОВАТЬ: изменить getMessage
в getLocalizedMessage
как предположил Илия.
РЕДАКТИРОВАТЬ 2: Забыл упомянуть: этот подход не поддерживает изменение локали "на лету", но это преднамеренно (его можно реализовать, если вам это нужно).
Возможно, я что-то упускаю, но если пользователям действительно требуется относительно тихий файл журнала, почему бы вам просто не настроить свои журналы отладки для перехода в отдельное место?
Если этого недостаточно, перехватите фиксированное количество журналов отладки в оперативной памяти. Например, последние 500 записей. Затем, когда происходит что-то ужасное, выведите журналы отладки вместе с отчетом о проблеме. Вы не упоминаете свою структуру ведения журналов, но это было бы довольно легко сделать в Log4J.
Более того, если у вас есть разрешение пользователя, просто отправьте автоматический отчет об ошибке, а не войдите в систему. Недавно я помог некоторым людям справиться с труднодоступной ошибкой и сделал автоматическое создание отчетов об ошибках. Мы получили в 50 раз больше сообщений об ошибках, что делает проблему довольно легко найти.
Еще один хороший API для ведения журналов - SLF4J. Он также может быть настроен на перехват API журналов для Log4J, Java Util Logging и Jakarta Commons Logging. И его также можно настроить на использование различных реализаций ведения журналов, включая Log4J, Logback, Java Util Logging и одну или две другие. Это дает ему огромную гибкость. Он был разработан автором Log4J, чтобы стать его преемником.
Что касается этого вопроса, то в SLF4J API есть механизм объединения строковых выражений в сообщение журнала. Следующие вызовы эквивалентны, но второй процесс обработки примерно в 30 раз быстрее, если вы не выводите сообщения уровня отладки, поскольку избегается конкатенация:
logger.debug("The new entry is " + entry + ".");
logger.debug("The new entry is {}.", entry);
Также есть версия с двумя аргументами:
logger.debug("The new entry is {}. It replaces {}.", entry, oldEntry);
И для более чем двух вы можете передать массив Object следующим образом:
logger.debug("Value {} was inserted between {} and {}.",
new Object[] {newVal, below, above});
Это хороший краткий формат, который устраняет беспорядок.
Пример источника взят из FAQ по SLF4J.
Изменить: Вот возможный рефакторинг вашего примера:
try {
doSomething(someObject.getValue());
}
catch (BadThingsHappenException bthe) {
throw new UnhandledException(
MessageFormatter.format("An error occurred when setting value. [value={}]",
someObject.getValue()),
bthe);
}
Или, если этот шаблон встречается в нескольких местах, вы можете написать набор статических методов, которые фиксируют общность, например:
try {
doSomething(someObject.getValue());
}
catch (BadThingsHappenException bthe) {
throwFormattedException(logger, bthe,
"An error occurred when setting value. [value={}]",
someObject.getValue()));
}
и, конечно, метод также поместит отформатированное сообщение в регистратор для вас.
Взгляните на класс MemoryHandler из java.util.logging. Он действует как буфер между вашими вызовами log.$ Level() и фактическим выводом и передает содержимое буфера в вывод, только если выполняется какое-то условие.
Например, вы можете настроить его на вывод содержимого только в том случае, если он увидит сообщение об ошибке ERROR. Затем вы можете безопасно выводить сообщения уровня DEBUG, и никто не увидит их, если не произойдет фактическая ошибка, а затем все сообщения будут записаны в файл журнала.
Я предполагаю, что есть подобные реализации для других каркасов журналирования.
РЕДАКТИРОВАТЬ: Одной из возможных проблем с этим подходом является потеря производительности при генерации всех сообщений отладки (см. Комментарий @djna). Из-за этого может быть хорошей идеей сделать уровень ведения журнала в буфере настраиваемым - в рабочем режиме он должен быть INFO или выше, и только если вы активно выискиваете проблему, ее можно отключить до DEBUG.
Один из вариантов, о котором никто еще не упоминал, - это использовать регистратор, который регистрирует данные в буфере в памяти и только при определенных обстоятельствах помещает информацию в фактическую цель журнала (например, регистрируется сообщение об ошибке).
Если вы используете средства ведения журналов JDK 1.4, MemoryHandler делает именно это. Я не уверен, что система ведения журнала, которую вы используете, делает это, но я полагаю, вы должны быть в состоянии реализовать свой собственный appender/handler/ что угодно, что делает что-то подобное.
Кроме того, я просто хочу отметить, что в вашем исходном примере, если ваша задача связана с областью действия переменной, вы всегда можете определить блок, чтобы уменьшить область действия вашей переменной:
{
String myValue = null;
try {
myValue = someObject.getValue();
doSomething(myValue);
}
catch (BadThingsHappenException bthe) {
String pattern = "An error occurred when setting value. [value={}]";
// note that the format method below doesn't barf on nulls
String detail = MessageFormatter.format(pattern, myValue);
// consider this a RuntimeException wrapper class
throw new UnhandledException(detail, bthe);
}
}
Помимо вашего примера, который объявляет локальные поля за пределами try
блок для того, чтобы быть доступным внутри catch
блок, один очень простой способ справиться с этим, чтобы сбросить состояние класса в Exception
используя переопределенный класс toString
метод. Конечно, это полезно только в Class
Если они поддерживают состояние.
try {
setMyValue(someObject.getValue());
doSomething(getMyValue());
}
catch (BadThingsHappenException bthe) {
// consider this a RuntimeException wrapper class
throw new UnhandledException(toString(), bthe);
}
Ваш toString()
нужно будет переопределить:
public String toString() {
return super.toString() + "[myValue: " + getMyValue() +"]";
}
редактировать:
другая идея:
Вы могли бы поддерживать состояние в ThreadLocal
контекст отладки. Предположим, вы создали класс с именем MyDebugUtils
который содержит ThreadLocal
который содержит карту на тему. Вы разрешаете статический доступ к этому ThreadLocal
и методы обслуживания (т. е. для очистки контекста после завершения отладки).
Интерфейс может быть:
public static void setValue(Object key, Object value)
public static void clearContext()
public static String getContextString()
и в нашем примере:
try {
MyDebugUtils.setValue("someObeject.value", someObject.getValue());
doSomething(someObject.getValue());
} catch (BadThingsHappenException bthe) {
// consider this a RuntimeException wrapper class
throw new UnhandledException(MyDebugUtils.getContextString(), bthe);
} finally {
MyDebugUtils.clearContext();
}
Там могут быть некоторые проблемы, которые вы хотели бы решить, например, обработка случаев, когда ваш doSomething
Метод также содержит try/catch/finally
набор, который очищает контекст отладки. Это можно было бы сделать, допуская более тонкую гранулярность в контекстной карте, чем просто поток в процессе:
public static void setValue(Object contextID, Object key, Object value)
public static void clearContext(Object contextID)
public static String getContextString(Object contextID)
Я создал комбинацию клавиш в eclipse для создания блока catch.
logmsg в качестве ключа, и результат будет
catch(SomeException se){
String msg = ""; //$NON-NLS-1$
Object[] args = new Object[]{};
throw new SomeException(Message.format(msg, args), se);
}
Вы можете поместить в сообщение столько информации, сколько захотите, например:
msg = "Dump:\n varA({0}), varB({1}), varC({2}), varD({3})";
args = new Object[]{varA, varB, varC, varD};
Или некоторые пользовательские данные
msg = "Please correct the SMTP client because ({0}) seems to be wrong";
args = new Object[]{ smptClient };
Вам следует подумать об использовании log4j в качестве регистратора, чтобы вы могли распечатывать свои состояния в любом месте. С помощью параметров DEBUG, INFO, ERROR вы можете определить, сколько журналов вы хотите видеть в вашем файле журнала.
При доставке приложения вы устанавливаете уровень журнала ERROR, но когда вы хотите отладить приложение, вы можете использовать DEBUG по умолчанию.
Когда вы используете регистратор, вам нужно только напечатать полную информацию в вашем исключении, потому что состояние некоторых переменных вы напечатаете в файле журнала, прежде чем вызывать критический блок try... catch.
String msg = "Dump:\n varA({0}), varB({1}), varC({2}), varD({3})";
Object[] args = new Object[]{varA, varB, varC, varD};
logger.debug(Message.format(msg, args));
try{
// do action
}catch(ActionException ae){
msg = "Please correct the SMTP client because ({0}) seems to be wrong";
args = new Object[]{ smptClient };
logger.error(Message.format(msg, args), se);
throw new SomeException(Message.format(msg, args), se);
}
Вы ответили на свой вопрос. Если вы хотите передать состояние в исключение, вам нужно где-то сохранить его.
Вы упомянули добавление дополнительных переменных для этого, но вам не понравились все дополнительные переменные. Кто-то еще упомянул MemoryHandler как буфер (состояние удержания) между регистратором и приложением.
Это все одна и та же идея. Создайте объект, который будет содержать состояние, которое вы хотите показать в своем исключении. Обновите этот объект по мере выполнения вашего кода. Если возникает ошибка, передайте этот объект в исключение.
Исключения уже делают это с StackTraceElements. Каждый поток хранит список трассировки стека (метод, файл, строка), который представляет его "состояние". Когда происходит исключение, он передает трассировку стека в исключение.
То, что вы, кажется, хотите, это также копия всех локальных переменных.
Это будет означать создание объекта для хранения всех ваших местных жителей и использование этого объекта вместо местных. Затем передача объекта в исключение.
Почему бы не сохранить локальную копию / список всех сообщений, которые были бы помещены в журнал отладки, если бы он был включен, и передать это пользовательскому исключению при его выдаче? Что-то вроде:
static void logDebug(String message, List<String> msgs) {
msgs.add(message);
log.debug(message);
}
//...
try {
List<String> debugMsgs = new ArrayList<String>();
String myValue = someObject.getValue();
logDebug("Value: " + myValue, debugMsgs);
doSomething(myValue);
int x = doSomething2();
logDebug("doSomething2() returned " + x, debugMsgs);
}
catch (BadThingsHappenException bthe) {
// at the point when the exception is caught,
// debugMsgs contains some or all of the messages
// which should have gone to the debug log
throw new UnhandledException(bthe, debugMsgs);
}
Ваш класс исключений может использовать это List
параметр при формировании getMessage()
:
public class UnhandledException extends Exception {
private List<String> debugMessages;
public UnhandledException(String message, List<String> debugMessages) {
super(message);
this.debugMessages = debugMessages;
}
@Override
public String getMessage() {
//return concatentation of super.getMessage() and debugMessages
}
}
Использование этого было бы утомительно - так как вам нужно было бы объявлять локальную переменную в каждой попытке / уловке, где вы хотели получить этот тип информации - но это может стоить того, если у вас есть только несколько критических разделов кода, в которых Вы хотели бы сохранить эту информацию о состоянии на исключение.
Что касается типа отладочной информации, которая вам нужна, почему бы вам просто не записать значение в журнал и не беспокоиться о локальной пробе / вылове. Просто используйте файл конфигурации Log4J, чтобы указать ваши сообщения отладки на другой журнал, или используйте бензопилу, чтобы вы могли удаленно следить за сообщениями журнала. Если все это терпит неудачу, возможно, вам нужен новый тип сообщения журнала, чтобы добавить его в debug()/info()/warn()/error()/fatal(), чтобы у вас был больший контроль над тем, какие сообщения куда отправляются. Это может быть в том случае, если определение дополнений в файле конфигурации log4j нецелесообразно из-за большого количества мест, в которые необходимо вставить этот тип журнала отладки.
Пока мы обсуждаем эту тему, вы затронули одну из моих любимых мозолей. Создание нового исключения в блоке catch - это запах кода.
Catch(MyDBException eDB)
{
throw new UnhandledException("Something bad happened!", eDB);
}
Поместите сообщение в журнал и затем сбросьте исключение. Создание исключений стоит дорого и может легко скрыть полезную информацию отладки.
Во-первых, неопытные программисты и те, кто любит вырезать-вставить-вставить (или begin-mark-bug, end-mark-bug, copy-bug, copy-bug, copy-bug), могут легко преобразоваться в это:
Catch(MyDBException eDB)
{
throw new UnhandledException("Something bad happened!");
}
Теперь вы потеряли оригинальную трассировку стека. Даже в первом случае, если исключение обертки не обрабатывает обернутое исключение должным образом, вы все равно можете потерять детали исходного исключения, наиболее вероятна трассировка стека.
Повторное исключение может быть необходимым, но я обнаружил, что оно должно обрабатываться более широко и как стратегия для взаимодействия между уровнями, например, между вашим бизнес-кодом и уровнем персистентности, например:
Catch(SqlException eDB)
{
throw new UnhandledAppException("Something bad happened!", eDB);
}
и в этом случае блок catch для исключения UnhandledAppException находится намного дальше по стеку вызовов, где мы можем дать пользователю указание на то, что ему либо нужно повторить свое действие, сообщить об ошибке или что-то еще.
Это позволило нашему коду main() сделать что-то вроде этого
catch(UnhandledAppException uae)
{
\\notify user
\\log exception
}
catch(Throwable tExcp)
{
\\for all other unknown failures
\\log exception
}
finally
{
\\die gracefully
}
Выполнение этого означало, что локальный код мог перехватывать немедленные и восстанавливаемые исключения, при которых могли быть сделаны журналы отладки, и исключение не должно быть переброшено Это было бы как для DivideByZero или, возможно, ParseException какого-то рода.
Что касается предложений "throws", то наличие стратегии исключений на основе уровня означало возможность ограничения количества типов исключений, которые должны быть перечислены для каждого метода.
Если вы хотите как-то обработать детали сообщения об ошибке, вы можете:
Используйте XML-текст в качестве сообщения, чтобы получить структурированный способ:
throw new UnhandledException(String.format( "<e><m>Unexpected things</m><value>%s</value></e>", value), bthe);
Используйте свои собственные (и по одному для каждого случая) типы исключений, чтобы захватить переменную информацию в именованные свойства:
throw new UnhandledValueException("Unexpected value things", value, bthe);
Или же вы можете включить его в необработанное сообщение, как предлагают другие.