Как можно передать полезную информацию о состоянии исключению в Java? - PullRequest
15 голосов
/ 11 июля 2009

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

Я заметил два шаблона в 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 ]

9 голосов
/ 11 июля 2009

Мы стремимся создавать наши наиболее важные классы исключений для конкретных приложений, используя некоторые специальные конструкторы, некоторые константы и 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 new MyException(MyException.PROBLEMCODETWO, new Object[] {parameter}, bthe))
  • Сообщения об исключениях "централизованы", могут легко поддерживаться и "интернационализироваться"

РЕДАКТИРОВАТЬ: изменить getMessage на getLocalizedMessage, как предложил Илия.

EDIT2: Забыл упомянуть: этот подход не поддерживает изменение локали "на лету", но он намеренный (его можно реализовать, если вам это нужно).

6 голосов
/ 13 июля 2009

Возможно, я что-то упускаю, но если пользователям действительно нужен относительно тихий файл журнала, почему бы вам просто не настроить свои журналы отладки для перехода в отдельное место?

Если этого недостаточно, перехватите фиксированное количество журналов отладки в оперативной памяти. Например, последние 500 записей. Затем, когда происходит что-то ужасное, выведите журналы отладки вместе с отчетом о проблеме. Вы не упоминаете свою структуру ведения журналов, но это было бы довольно легко сделать в Log4J.

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

5 голосов
/ 11 июля 2009

Еще один хороший API для ведения журналов - SLF4J. Он также может быть настроен на перехват API журналов для Log4J, Java Util Logging и Jakarta Commons Logging. И он также может быть настроен на использование различных реализаций ведения журналов, включая Log4J, Logback, Java Util Logging и один или два других. Это дает ему огромную гибкость. Он был разработан автором Log4J, чтобы стать его преемником.

Относящийся к этому вопросу, API-интерфейс SLF4J имеет механизм для объединения строковых выражений в сообщение журнала. Следующие вызовы эквивалентны, но второй процесс обработки примерно в 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});

Это хороший краткий формат, который устраняет беспорядок.

Пример источника взят из SLF4J FAQ .

Редактировать: Вот возможный рефакторинг вашего примера:

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()));
}

и, конечно, метод также поместит отформатированное сообщение в логгер.

4 голосов
/ 15 июля 2009

Взгляните на класс MemoryHandler из java.util.logging. Он действует как буфер между вашими вызовами log. $ Level () и фактическим выводом и передает содержимое буфера в вывод, только если выполняется какое-то условие.

Например, вы можете настроить его на вывод содержимого только в том случае, если он увидит сообщение об ошибке ERROR. Затем вы можете безопасно выводить сообщения уровня DEBUG, и никто не увидит их, если не произойдет фактическая ошибка, а затем все сообщения будут записаны в файл журнала.

Я бы предположил, что есть аналогичные реализации для других каркасов журналирования.

РЕДАКТИРОВАТЬ: Одной из возможных проблем с этим подходом является потеря производительности при генерации всех сообщений отладки (см. Комментарий @djna). Из-за этого может быть хорошей идеей сделать уровень ведения журнала в буфере настраиваемым - в рабочем режиме он должен быть INFO или выше, и только если вы активно выискиваете проблему, ее можно отключить до DEBUG.

2 голосов
/ 19 июля 2009

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

Если вы используете средства ведения журналов 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);
    }
}
2 голосов
/ 13 июля 2009

Помимо вашего примера, который объявляет локальные поля вне блока 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() +"]";
}

edit:

другая идея:

Вы можете поддерживать состояние в контексте отладки 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)
1 голос
/ 18 июля 2009

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

Вы упомянули добавление дополнительных переменных для этого, но вам не понравились все дополнительные переменные. Кто-то еще упомянул MemoryHandler как буфер (состояние удержания) между регистратором и приложением.

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

Исключения уже делают это с StackTraceElements. Каждый поток хранит список трассировки стека (метод, файл, строка), который представляет его «состояние». Когда происходит исключение, он передает трассировку стека в исключение.

То, что вы, похоже, хотите, это также копия всех локальных переменных.

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

1 голос
/ 16 июля 2009

Почему бы не сохранить локальную копию / список всех сообщений, которые были бы помещены в журнал отладки, если бы он был включен, и передать это пользовательскому исключению при его выдаче? Что-то вроде:

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
    }
}

Использование этого было бы утомительно - так как вам нужно было бы объявлять локальную переменную в каждую попытку / перехват, где вы хотели получить этот тип информации - но это может стоить, если у вас есть всего несколько критических разделов кода, в которых вы хотите сохранить эту информацию о состоянии для исключения.

1 голос
/ 13 июля 2009

Я создал комбинацию клавиш в 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);
}
0 голосов
/ 15 июля 2009

Что касается нужного вам типа отладочной информации, почему бы вам просто не всегда записывать значение в журнал и не слишком беспокоиться о локальном try / catch. Просто используйте файл конфигурации 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», то наличие стратегии исключений на основе уровня означало возможность ограничения числа типов исключений, которые должны быть перечислены для каждого метода.

...