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

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

我一直在注意Java中的两种模式,用于记录在发生exception时可能对开发人员有用的信息。

以下模式似乎很常见。 基本上,您只需根据需要在线记录您的记录器日志信息,以便在发生exception时获得日志跟踪。

try { String myValue = someObject.getValue(); logger.debug("Value: {}", myValue); doSomething(myValue); } catch (BadThingsHappenException bthe) { // consider this a RuntimeException wrapper class throw new UnhandledException(bthe); } 

上述方法的缺点是,如果您的用户需要相对安静的日志并且需要高级别的可靠性,而他们无法“在调试模式下再次尝试”,则exception消息本身包含的数据不足以对开发者有用。

我看到的下一个模式试图缓解这个问题,但看起来很难看:

 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存储键值对,然后将其转储到exception消息中。 我不确定我喜欢这种方法,因为它似乎创造了代码膨胀。

那里有一些我遗漏的Java vodoo吗? 你如何处理你的exception状态信息?

我们倾向于使用一些特殊的构造函数,一些常量和一个ResourceBundle来创建我们最重要的特定于应用程序的运行时exception类。

示例代码段:

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

在属性文件中,我们指定了exception描述,例如:

  problemCodeOne=Simple exception message problemCodeTwo=Parameterized exception message for {0} value 

使用这种方法

  • 我们可以使用非常易读和易懂的throw子句( throw new MyException(MyException.PROBLEMCODETWO, new Object[] {parameter}, bthe)
  • exception消息是“集中的”,可以轻松维护和“国际化”

编辑:将Elijah建议的getMessage更改为getLocalizedMessage

EDIT2:忘了提一下:这种方法不支持“动态”更改Locale,但它是有意的(可以在需要时实现)。

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

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

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

另一个好的日志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())); } 

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

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

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

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

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

除了在try块之外声明局部字段以便在catch块内可访问的示例之外,处理它的一种非常简单的方法是使用类的重写toString方法将类的状态转储到Exception 。 当然,这仅适用于维护状态的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 ,其中包含每个线程的Map。 您允许静态访问此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) 

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

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

我已经在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); } 

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

 static void logDebug(String message, List msgs) { msgs.add(message); log.debug(message); } //... try { List debugMsgs = new ArrayList(); 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); } 

在形成getMessage()时,您的exception类可以使用此List参数:

 public class UnhandledException extends Exception { private List debugMessages; public UnhandledException(String message, List debugMessages) { super(message); this.debugMessages = debugMessages; } @Override public String getMessage() { //return concatentation of super.getMessage() and debugMessages } } 

使用它会很繁琐 – 因为你必须在你想要这种类型的信息的每一个 try / catch中声明局部变量 – 但是如果你只有几个关键的代码段,那么它可能是值得的。你想在exception上维护这个状态信息。

你是在自问自答。 如果要将状态传递给exception,则需要将状态存储在某处。

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

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

使用StackTraceElements的例外已经做到了这一点。 每个线程都保留一个表示其“状态”的堆栈跟踪列表(方法,文件,行)。 发生exception时,它会将堆栈跟踪传递给exception。

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

这意味着使对象保留所有本地并使用该对象,而不是直接使用本地对象。 然后将对象传递给exception。

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

  • 使用XML文本作为消息,因此您可以获得结构化方式:

     throw new UnhandledException(String.format( "Unexpected things%s", value), bthe); 
  • 使用您自己的(以及每种情况下的一个)exception类型将变量信息捕获到命名属性中:

     throw new UnhandledValueException("Unexpected value things", value, bthe); 

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

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

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

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

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

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

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

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

重新抛出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 } 

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

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