Java中的异常丰富
异常丰富是异常包装的替代方法。异常包装具有可以弥补异常丰富的两个缺点。这些缺点是:
- 异常包装可能会导致非常长的堆栈跟踪,其中包括用于包装层次结构中每个异常的一个堆栈跟踪。通常,只有根堆栈跟踪才是有趣的。然后,其余的堆栈跟踪就很烦人了。
- 异常消息分散在堆栈跟踪中。异常消息通常显示在堆栈跟踪上方。当几个异常在层次结构中相互包装时,所有这些消息都在堆栈跟踪之间散布开来。这使得更难确定发生了什么错误以及错误发生时程序试图执行的操作。换句话说,很难确定在什么情况下发生了错误。该错误可能是在PersonDao类中发生的,但是当它失败时是从servlet还是从Web服务调用的?
在异常丰富中,我们不会包装异常。相反,我们可以将上下文信息添加到原始异常中,然后将其重新抛出。抛出异常不会重置嵌入在异常中的堆栈跟踪。
这是一个例子:
public void method2() throws EnrichableException{ try{ method1(); } catch(EnrichableException e){ e.addInfo("An error occurred when trying to ..."); throw e; } } public void method1() throws EnrichableException { if(...) throw new EnrichableException( "Original error message"); }
如我们所见,method1()抛出一个EnrichableException,它是可填充异常的超类。这不是标准的Java异常,因此我们必须自己创建它。本文末尾有一个示例EnrichableException。
请注意method2()如何在捕获的EnrichableException上调用addInfo()方法,然后将其重新抛出。随着异常在调用堆栈中的传播,每个catch块可以在必要时向异常添加相关信息。
使用这种简单的技术,我们将只获得单个堆栈跟踪,并且仍然获得调查异常原因所需的任何相关上下文信息。
唯一错误代码
有时要求在应用程序中引发的每个错误都必须由唯一的错误代码来标识。这可能会有点问题,因为在组件中会引发一些错误,这些错误会在整个应用程序中重复使用。因此,对于抛出该异常的组件来说,异常可能看起来是相同的,但是发生异常的上下文是不同的。
这是一个例子:
public void method3() throws EnrichableException{ try{ method1(); } catch(EnrichableException e){ e.addInfo("An error occurred when trying to ..."); throw e; } } public void method2() throws EnrichableException{ try{ method1(); } catch(EnrichableException e){ e.addInfo("An error occurred when trying to ..."); throw e; } } public void method1() throws EnrichableException { if(...) throw new EnrichableException( "ERROR1", "Original error message"); }
请注意,method1()如何将代码" ERROR1"添加到抛出的EnrichableException中,以唯一地标识该错误原因。但也要注意,method2()和method3()都调用了method1()。尽管无论调用method2()和method3()的哪一个,该错误对于method1()似乎都是相同的,但对于开发人员研究该错误而言,了解这一点可能很重要。错误代码" ERROR1"足以确定错误发生的位置,但不能确定错误发生的背景。
解决此问题的方法是,向异常中添加唯一的上下文错误代码,方法与添加其他上下文信息相同。这是一个示例,其中addInfo()方法已被更改以适应这种情况:
public void method3() throws EnrichableException{ try{ method1(); } catch(EnrichableException e){ e.addInfo("METHOD3", "ERROR1", "An error occurred when trying to ..."); throw e; } } public void method2() throws EnrichableException{ try{ method1(); } catch(EnrichableException e){ e.addInfo("METHOD2", "ERROR1", "An error occurred when trying to ..."); throw e; } } public void method1() throws EnrichableException { if(...) throw new EnrichableException( "METHOD1", "ERROR1", "Original error message"); }
两个新参数已添加到addInfo()方法和EnrichableException的构造函数中。第一个参数是标识发生错误的上下文的代码。第二个参数是该上下文中的唯一错误代码。从method2()调用时,method1()引发的异常的错误标识现在看起来像这样:
[METHOD2:ERROR1][METHOD1:ERROR1]
当从method3()调用method1()时,错误标识将如下所示:
[METHOD3:ERROR1][METHOD1:ERROR1]
如我们所见,现在可以将通过method2()从method1()引发的异常与通过method3()从method1()引发的相同异常区分开。
我们可能并不总是需要额外的上下文错误代码,但是当我们执行本节中介绍的解决方案时,可以选择这样做。
包装不可丰富的异常
我们可能并非总是能够避免异常包装。如果应用程序中的组件引发了不可扩展的检查异常,则可能必须将其包装在可扩展异常中。这是一个示例,其中method1()捕获一个不可丰富的异常并将其包装在一个可丰富的异常中,并引发该可丰富的异常:
public void method1() throws EnrichableException { try{ ... call some method that throws an IOException ... } catch(IOException ioexception) throw new EnrichableException( ioexception, "METHOD1", "ERROR1", "Original error message"); }
未经检查的EnrichableException
我过去一直赞成检查异常,但是在过去的几年中,我的看法发生了变化。现在,我觉得检查异常更多的是麻烦而不是帮助。因此,我希望通过扩展RuntimeException使EnrichableException不受检查。
在"已检查的异常与未检查的异常"一文中,对已检查和未检查的异常进行了更全面的讨论。
异常丰富和可插入异常处理程序
像其他任何异常类型一样,可以将可插入异常处理程序与丰富的异常一起使用。如果使用前面描述的唯一错误代码,则必须将这些代码作为参数添加到异常处理程序接口中。这是一个示例异常处理程序接口,支持这些独特的错误代码:
public interface ExceptionHandler{ public void handle(String contextCode, String errorCode, String errorText, Throwable t) public void raise(String contextCode, String errorCode, String errorText); }
程序中捕获的异常将传递到handleException(),该异常将决定要抛出的具体异常。在这种情况下,将抛出EnrichableException。如果未选中EnrichableException,则无需在handleException()方法中对其进行声明。
一个示例EnrichableException
以下是可扩展异常的示例,我们可以将其用作自己的可扩展异常的模板。我们可能需要更改类定义以适合我们自己的需求。如本文前面所述,该异常被设计为使用唯一的错误代码。
代码下面是使用EnrichableException的示例应用程序,以及从该应用程序生成的堆栈跟踪。
import java.util.ArrayList; import java.util.List; public class EnrichableException extends RuntimeException { public static final long serialVersionUID = -1; protected List<InfoItem> infoItems = new ArrayList<InfoItem>(); protected class InfoItem{ public String errorContext = null; public String errorCode = null; public String errorText = null; public InfoItem(String contextCode, String errorCode, String errorText){ this.errorContext = contextCode; this.errorCode = errorCode; this.errorText = errorText; } } public EnrichableException(String errorContext, String errorCode, String errorMessage){ addInfo(errorContext, errorCode, errorMessage); } public EnrichableException(String errorContext, String errorCode, String errorMessage, Throwable cause){ super(cause); addInfo(errorContext, errorCode, errorMessage); } public EnrichableException addInfo( String errorContext, String errorCode, String errorText){ this.infoItems.add( new InfoItem(errorContext, errorCode, errorText)); return this; } public String getCode(){ StringBuilder builder = new StringBuilder(); for(int i = this.infoItems.size()-1 ; i >=0; i--){ InfoItem info = this.infoItems.get(i); builder.append('['); builder.append(info.errorContext); builder.append(':'); builder.append(info.errorCode); builder.append(']'); } return builder.toString(); } public String toString(){ StringBuilder builder = new StringBuilder(); builder.append(getCode()); builder.append('\n'); //append additional context information. for(int i = this.infoItems.size()-1 ; i >=0; i--){ InfoItem info = this.infoItems.get(i); builder.append('['); builder.append(info.errorContext); builder.append(':'); builder.append(info.errorCode); builder.append(']'); builder.append(info.errorText); if(i>0) builder.append('\n'); } //append root causes and text from this exception first. if(getMessage() != null) { builder.append('\n'); if(getCause() == null){ builder.append(getMessage()); } else if(!getMessage().equals(getCause().toString())){ builder.append(getMessage()); } } appendException(builder, getCause()); return builder.toString(); } private void appendException( StringBuilder builder, Throwable throwable){ if(throwable == null) return; appendException(builder, throwable.getCause()); builder.append(throwable.toString()); builder.append('\n'); }
[L1:E1][L2:E2][L3:E3] [L1:E1]Error in level 1, calling level 2 [L2:E2]Error in level 2, calling level 3 [L3:E3]Error at level 3 java.lang.IllegalArgumentException: incorrect argument passed at exception.ExceptionTest.handle(ExceptionTest.java:8) at exception.ExceptionTest.level3(ExceptionTest.java:49) at exception.ExceptionTest.level2(ExceptionTest.java:38) at exception.ExceptionTest.level1(ExceptionTest.java:29) at exception.ExceptionTest.main(ExceptionTest.java:21) Caused by: java.lang.IllegalArgumentException: incorrect argument passed at exception.ExceptionTest.level4(ExceptionTest.java:54) at exception.ExceptionTest.level3(ExceptionTest.java:47) ... 3 more
public class ExceptionTest { protected ExceptionHandler exceptionHandler = new ExceptionHandler(){ public void handle(String errorContext, String errorCode, String errorText, Throwable t){ if(! (t instanceof EnrichableException)){ throw new EnrichableException( errorContext, errorCode, errorText, t); } else { ((EnrichableException) t).addInfo( errorContext, errorCode, errorText); } } public void raise(String errorContext, String errorCode, String errorText){ throw new EnrichableException( errorContext, errorCode, errorText); } }; public static void main(String[] args){ ExceptionTest test = new ExceptionTest(); try{ test.level1(); } catch(Exception e){ e.printStackTrace(); } } public void level1(){ try{ level2(); } catch (EnrichableException e){ this.exceptionHandler.handle( "L1", "E1", "Error in level 1, calling level 2", e); throw e; } } public void level2(){ try{ level3(); } catch (EnrichableException e){ this.exceptionHandler.handle( "L2", "E2", "Error in level 2, calling level 3", e); throw e; } } public void level3(){ try{ level4(); } catch(Exception e){ this.exceptionHandler.handle( "L3", "E3", "Error at level 3", e); } } public void level4(){ throw new IllegalArgumentException("incorrect argument passed"); } }