Что такое хороший способ передачи полезной информации о состоянии в исключение в 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, которого я не вижу? Как вы обрабатываете информацию о состоянии исключения?

Ответ 1

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

EDIT: измените getMessage на getLocalizedMessage, как предложил Илия.

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

Ответ 2

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

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

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

Ответ 3

Другим хорошим протоколом регистрации является SLF4J. Он может быть сконфигурирован так же, как перехватывать log 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);

И для более чем двух вы можете передать массив объектов следующим образом:

logger.debug("Value {} was inserted between {} and {}.", 
             new Object[] {newVal, below, above});

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

Пример источника из часто задаваемые вопросы 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()));
}

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

Ответ 4

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

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

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

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

Ответ 5

Помимо вашего примера, который объявляет локальные поля вне блока try, чтобы быть доступным внутри блока catch, один очень простой способ справиться с этим - сбросить состояние класса вне в Exception, используя класс, переопределенный метод toString. Конечно, это полезно только в Class es, которые поддерживают состояние.

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, который очищает контекст отладки. Это можно было бы обработать, разрешив более тонкую детализацию в контекстной карте, чем просто Thread в процессе:

public static void setValue(Object contextID, Object key, Object value)
public static void clearContext(Object contextID)
public static String getContextString(Object contextID)

Ответ 6

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

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

Ответ 7

Я создал комбинацию клавиш в 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 по умолчанию.

Когда вы используете регистратор, вам нужно распечатать всю информацию, содержащуюся в вашем exceotion, потому что состояние некоторых переменных, которые вы будете печатать в файле журнала, прежде чем вы вызовете критический блок 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);
}

Ответ 8

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

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

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

Ответ 9

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

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

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

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

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

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

Ответ 10

Если вы хотите каким-то образом обработать детали сообщения об ошибке, вы можете:

  • Используйте текст 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);
    

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

Ответ 11

Что касается типа отладочной информации, в которой вы нуждаетесь, почему бы вам просто не регистрировать значение и не беспокоиться о локальном try/catch. Просто используйте конфигурационный файл Log4J, чтобы указать ваши отладочные сообщения на другой журнал или использовать бензопилу, чтобы вы могли удаленно отслеживать сообщения журнала. Если все это не удастся, возможно, вам понадобится новый тип сообщения журнала для добавления в debug()/info()/warn()/error()/fatal(), чтобы вы могли больше контролировать, какие сообщения отправляться туда. Это будет так, когда определение добавлений в конфигурационном файле log4j нецелесообразно из-за большого количества мест, где необходимо вставить этот тип ведения журнала отладки.

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

Catch(MyDBException eDB)
{
    throw new UnhandledException("Something bad happened!", eDB);
}

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

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

Catch(MyDBException eDB)
{
    throw new UnhandledException("Something bad happened!");
}

Теперь вы потеряли исходный стек. Даже в первом случае, если исключение обертывания не будет правильно обработано обернутое исключение, вы все равно можете потерять сведения об исходном исключении, наиболее вероятным будет stacktrace.

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

Catch(SqlException eDB)
{
    throw new UnhandledAppException("Something bad happened!", eDB);
}

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

Это позволит нашему главному() коду сделать что-то вроде этого

catch(UnhandledAppException uae)
{
    \\notify user
    \\log exception
}
catch(Throwable tExcp)
{
    \\for all other unknown failures
    \\log exception

}
finally
{
    \\die gracefully
}

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

Что касается предложений "throws", то наличие стратегии исключения на основе слоев означает возможность ограничить количество типов исключений, которые должны быть перечислены для каждого метода.