将有用的状态信息传递给Java中的异常有什么好方法?

时间:2009-07-11 02:29:21

标签: java exception logging error-handling

我最初注意到我的问题引起了一些困惑。我不是在询问如何配置记录器,也不是如何正确使用记录器,而是如何捕获本地记录级别低于异常消息中当前日志记录级别记录的所有信息。

我一直在注意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块的范围之外声明这么多变量。特别是,当我必须处理非常复杂的状态时。

我见过的唯一另一种方法是使用Map存储键值对,然后将其转储到异常消息中。我不确定我是否喜欢这种方法,因为它似乎会造成代码臃肿。

那里有一些我错过的Java vodoo吗?你如何处理你的异常状态信息?

11 个答案:

答案 0 :(得分:9)

我们倾向于使用一些特殊的构造函数,一些常量和一个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

EDIT2:忘记提及:此方法不支持“动态”更改区域设置,但它是故意的(如果需要,可以实现)。

答案 1 :(得分:6)

也许我错过了一些东西,但是如果用户真的需要一个相对安静的日志文件,你为什么不把调试日志配置成一个单独的位置?

如果这还不够,那么在RAM中捕获一定量的调试日志。例如,最后500个条目。然后,当发生丑陋的事情时,将调试日志与问题报告一起转储。你没有提到你的日志框架,但在Log4J中这很容易做到。

更好的是,假设您拥有用户的权限,只需发送自动错误报告而不是记录。我最近帮助一些人发现了一个难以发现的错误并自动报告错误。我们获得了错误报告数量的50倍,这使问题很容易找到。

答案 2 :(得分:5)

另一个好的日志API是SLF4J。它还可以配置为拦截Log4J,Java Util Logging和Jakarta Commons Logging的日志API。它还可以配置为使用各种日志记录实现,包括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});

这是一种很好的简洁格式,可以消除混乱。

示例来源来自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()));
}

当然,该方法也会将格式化的消息输出到记录器上。

答案 3 :(得分:4)

查看java.util.logging中的MemoryHandler类。它充当日志。$ level()调用和实际输出之间的缓冲区,只有在满足某些条件时才会将其缓冲区内容传递给输出。

例如,您可以将其配置为仅在看到ERROR级别消息时转储内容。然后您可以安全地输出DEBUG级别的消息,除非发生实际错误,否则没有人会看到它们,然后所有消息都写入日志文件。

我猜它有其他日志框架的类似实现。

编辑:这种方法的一个可能问题是在生成所有调试消息时性能丢失(请参阅@djna注释)。因此,最好使进入缓冲区的日志记录级别可配置 - 在生产中它应该是INFO或更高,并且只有当您正在积极寻找问题时才可以将其调低为DEBUG。

答案 4 :(得分:2)

除了在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集的情况。这可以通过在上下文Map中允许更精细的粒度来处理,而不仅仅是过程中的Thread:

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

答案 5 :(得分:2)

似乎没有人提到的一个选项是使用记录到内存缓冲区的记录器,并且仅在某些情况下将信息推送到实际的日志目标(例如,记录错误级别消息)。

如果您正在使用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);
    }
}

答案 6 :(得分:1)

我在eclipse中创建了一个用于创建catch块的关键组合。

logmsg作为键,结果将是

catch(SomeException se){
   String msg = ""; //$NON-NLS-1$
   Object[] args = new Object[]{};

   throw new SomeException(Message.format(msg, args), se);
}

您可以在Message中输入任意数量的信息,如:

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,但是当您想要对应用程序进行debu时,您可以使用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);
}

答案 7 :(得分:1)

为什么不保留所有已经进入调试日志的消息的本地副本/列表(如果已启用),并在抛出它时将其传递给自定义异常?类似的东西:

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

使用它会很繁琐 - 因为你必须在每个 try / catch中声明局部变量你想要这种类型的信息 - 但是如果你有可能是值得的只有几个关键的代码部分,您希望在这些部分中保留关于异常的状态信息。

答案 8 :(得分:1)

你回答了自己的问题。如果要将状态传递给异常,则需要将状态存储在某处。

您已经提到添加额外的变量来执行此操作,但不喜欢所有额外的变量。 其他人提到MemoryHandler作为记录器和应用程序之间的缓冲区(保持状态)。

这些都是一样的想法。创建一个对象,该对象将保存您希望在异常中显示的状态。在代码执行时更新该对象。如果发生错误,则将该对象传递给异常。

异常已经通过StackTraceElements执行此操作。每个线程都保留一个表示其“状态”的堆栈跟踪列表(方法,文件,行)。发生异常时,它会将堆栈跟踪传递给异常。

您似乎想要的是所有局部变量的副本。

这意味着制作一个对象来保存所有本地人并使用该对象,而不是直接使用本地人。然后将对象传递给异常。

答案 9 :(得分:0)

如果您想以某种方式处理错误消息的详细信息,您可以:

  • 使用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);
    

否则您可以将其包含在原始邮件中,如其他人所建议的那样。

答案 10 :(得分:0)

至于你需要的调试信息的类型,为什么你不总是记录值,不要打扰本地的try / catch。只需使用Log4J配置文件将调试消息指向其他日志,或使用电锯,以便远程跟踪日志消息。如果所有失败可能你需要一个新的日志消息类型添加到debug()/ info()/ warn()/ error()/ fatal(),这样你就可以更好地控制哪些消息被发送到哪里。在log4j配置文件中定义appender是不切实际的,因为需要插入此类调试日志的地方数量很多。

当我们谈论这个话题时,你已经触及了我的一个小小的烦恼。在catch块中构造一个新的异常是代码气味。

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

将消息放入日志中,然后重新抛出异常。构造异常是昂贵的,并且可以轻松隐藏有用的调试信息。

首先,缺乏经验的程序员和那些喜欢剪切粘贴(或开始标记错误,结束标记错误,复制错误,复制错误,复制错误)的人可以轻松转换为此:

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

现在你已经丢失了原来的堆栈跟踪。即使在第一种情况下,除非包装Exception正确处理包装的异常,否则您仍然可能丢失原始异常的详细信息,堆栈跟踪最有可能。

重新抛出异常可能是必要的,但我发现它应该更普遍地处理,并作为层之间通信的策略,例如业务代码和持久层之间的沟通,如下所示:

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

并且在这种情况下,UnhandledAppException的catch块在调用堆栈的更远处,我们可以向用户指示他们需要重试其操作,报告错误或其他任何内容。

这让我们的main()代码执行类似这样的操作

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

}
finally
{
    \\die gracefully
}

这样做意味着本地代码可以捕获立即和可恢复的异常,其中可以完成调试日志并且不必重新抛出异常。这对于DivideByZero或者某种类型的ParseException也是如此。

对于“throws”子句,拥有基于图层的异常策略意味着能够限制必须为每个方法列出的异常类型的数量。