您的位置:首页 > 编程语言 > Java开发

Java异常机制及异常处理分析

2012-11-19 22:52 1441 查看
异常处理的主要问题就是何时(when)该如何(how)使用异常。

1. 异常的概念

什么是异常?运行时发生的可被捕获和处理的错误。

在java语言中,Exception是所有异常的父类。任何异常都扩展于Exception类,Exception就相当于一个错误类型。如果要定义一个新的错误类型就扩展一个新的Exception子类。采用异常的好处还在于可以精确的定位到导致程序出错的源代码位置,并获得详细的错误信息。

大体上说,有三种不同的“情景”会导致异常的抛出:

(1)编程错误导致异常(Exception due Programming errors): 这种情景下,异常往往处于编程错误(如:NullPointerException
或者 IllegalArgumentException),这时异常一旦抛出,客户端将变得无能为力。

(2)客户端代码错误导致异常(Exception due client code errors): 说白点就是客户端试图调用API不允许的操作。

(3)资源失败导致异常(Exception due to resource failures): 如内存不足或网络连接失败导致出现异常等,这些异常的出现客户端可以采取相应的措施来恢复应用程序的继续运行。

2. Java异常体系结构

Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。在Java API中已经定义了许多异常类,这些异常类分为两大类,错误Error和异常Exception。Java异常体系结构呈树状,其层次结构图如图 1所示:

图 1 Java异常体系结构
Thorwable类所有异常和错误的超类,有两个子类Error和Exception,分别表示错误和异常。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常,这两种异常有很大的区别,也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

从责任这个角度看Error属于JVM需要负担的责任、RuntimeException是程序应该负担的责任、checked exception 是具体应用负担的责任。

2.1 Error

Error是程序无法处理的错误,比如OutOfMemoryError、ThreadDeath等,Java虚拟机抛出一个Error对象,应用程序不捕获或抛出Errors对象,你可能永远不会遇到需要实例化Error的应用,这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

2.2 Exception

Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。

2.2.1 Checked exception

这类异常都是Exception的子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过,如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

一个checked exception强迫它的客户端可以抛出并捕获它,一旦客户端不能有效地处理这些被抛出的异常就会给程序的执行带来不期望的负担。

Checked exception还可能带来封装泄漏,看下面的代码:

public List getAllAccounts() throws FileNotFoundException, SQLException{

...

}

上边的方法抛出两个异常,客户端必须显示的对这两种异常进行捕获和处理,即使是在完全不知道这种异常到底是因为文件还是数据库操作引起的情况下。因此,此时的异常处理将导致方法和调用者之间不合适的耦合。

2.2.2 Unchecked exception

这类异常都是RuntimeException的子类,虽然RuntimeException同样也是Exception的子类,但是它们是特殊的,它们不能通过client code来试图解决,所以称为Unchecked exception,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

3. 常见异常处理方式分析

以下面这段代码分析异常处理的六个问题

代码:

1 OutputStreamWriter out = ...

2 java.sql.Connection conn = ...

3 try { // ⑸

4 Statement stat = conn.createStatement();

5 ResultSet rs = stat.executeQuery(

6 "select uid, name from user");

7 while (rs.next()){

9 out.println("ID:" + rs.getString("uid") // ⑹

10 ",姓名:" + rs.getString("name"));

11 }

12 conn.close(); // ⑶

13 out.close();

14 }

15 catch(Exception ex) // ⑵

16 {

17 ex.printStackTrace(); //⑴,⑷

18

3.1 丢弃异常

代码:15行-18行。

这段代码捕获了异常却不作任何处理,可以算得上Java编程中的杀手。如果你看到了这种丢弃(而不是抛出)异常的情况,可以百分之九十九地肯定代码存在问题(在极少数情况下,这段代码有存在的理由,但最好加上完整的注释,以免引起别人误解)。

这段代码的错误在于,异常(几乎)总是意味着某些事情不对劲了,我们不应该对程序发出的求救信号保持沉默和无动于衷。调用一下printStackTrace算不上“处理异常”。不错,调用printStackTrace对调试程序有帮助,但程序调试阶段结束之后,printStackTrace就不应再在异常处理模块中担负主要责任了。

那么,应该怎样改正呢?主要有四个选择:

(1) 处理异常。针对该异常采取一些行动,例如修正问题、提醒某个人或进行其他一些处理,要根据具体的情形确定应该采取的动作。再次说明,调用printStackTrace算不上已经“处理好了异常”。

(2) 重新抛出异常。处理异常的代码在分析异常之后,认为自己不能处理它,重新抛出异常也不失为一种选择。

(3) 把该异常转换成另一种异常。大多数情况下,这是指把一个低级的异常转换成应用级的异常(其含义更容易被用户了解的异常)。

(4) 不要捕获异常。

结论:既然捕获了异常,就要对它进行适当的处理,不要捕获异常之后又把它丢弃,不予理睬。

3.2 不指定具体的异常

代码:15行。

许多时候人们会被这样一种“美妙的”想法吸引:用一个catch语句捕获所有的异常,最常见的情形就是使用catch(Exception ex)语句。但实际上,在绝大多数情况下,这种做法不值得提倡,为什么呢?

要理解其原因,我们必须回顾一下catch语句的用途。catch语句表示我们预期会出现某种异常,而且希望能够处理该异常。异常类的作用就是告诉Java编译器我们想要处理的是哪一种异常。由于绝大多数异常都直接或间接从java.lang.Exception派生,catch(Exception ex)就相当于说我们想要处理几乎所有的异常。

再来看看前面的代码例子。我们真正想要捕获的异常是什么呢?最明显的一个是SQLException,这是JDBC操作中常见的异常。另一个可能的异常是IOException,因为它要操作OutputStreamWriter。显然,在同一个catch块中处理这两种截然不同的异常是不合适的。如果用两个catch块分别捕获SQLException和IOException就要好多了。这就是说,catch语句应当尽量指定具体的异常类型,而不应该指定涵盖范围太广的Exception类。

另一方面,除了这两个特定的异常,还有其他许多异常也可能出现。例如,如果由于某种原因,executeQuery返回了null,该怎么办?答案是让它们继续抛出,即不必捕获也不必处理。实际上,我们不能也不应该去捕获可能出现的所有异常,程序的其他地方还有捕获异常的机会,直至最后由JVM处理。

结论:在catch语句中尽可能指定具体的异常类型,必要时使用多个catch,不要试图处理所有可能出现的异常。

3.3 占用资源不释放

代码:3行-14行。

异常改变了程序正常的执行流程,这个道理虽然简单,却常常被人们忽视。如果程序用到了文件、Socket、JDBC连接之类的资源,即使遇到了异常,也要正确释放占用的资源。为此,Java提供了一个简化这类操作的关键词finally。

finally是样好东西:不管是否出现了异常,Finally保证在try/catch/finally块结束之前,执行清理任务的代码总是有机会执行。遗憾的是有些人却不习惯使用finally。

当然,编写finally块应当多加小心,特别是要注意在finally块之内抛出的异常,这是执行清理任务的最后机会,尽量不要再有难以处理的错误。

结论:保证所有资源都被正确释放,充分运用finally关键词。

3.4 不说明异常的详细信息

代码:3行-18行。

仔细观察这段代码:如果循环内部出现了异常,会发生什么事情?我们可以得到足够的信息判断循环内部出错的原因吗?不能。我们只能知道当前正在处理的类发生了某种错误,但却不能获得任何信息判断导致当前错误的原因。

printStackTrace的堆栈跟踪功能显示出程序运行到当前类的执行流程,但只提供了一些最基本的信息,未能说明实际导致错误的原因,同时也不易解读。

因此,在出现异常时,最好能够提供一些文字信息,例如当前正在执行的类、方法和其他状态信息,包括以一种更适合阅读的方式整理和组织printStackTrace提供的信息。

结论:在异常处理模块中提供适量的错误原因信息,组织错误信息使其易于理解和阅读。

3.5 过于庞大的try块

代码:3行-14行。

经常可以看到有人把大量的代码放入单个try块,实际上这不是好习惯。这种现象之所以常见,原因就在于有些人图省事,不愿花时间分析一大块代码中哪几行代码会抛出异常、异常的具体类型是什么。把大量的语句装入单个巨大的try块就象是出门旅游时把所有日常用品塞入一个大箱子,虽然东西是带上了,但要找出来可不容易。

一些新手常常把大量的代码放入单个try块,然后再在catch语句中声明Exception,而不是分离各个可能出现异常的段落并分别捕获其异常。这种做法为分析程序抛出异常的原因带来了困难,因为一大段代码中有太多的地方可能抛出Exception。

结论:尽量减小try块的体积。

3.6 输出数据不完整

代码:7行-11行。

不完整的数据是Java程序的隐形杀手。仔细观察这段代码,考虑一下如果循环的中间抛出了异常,会发生什么事情。循环的执行当然是要被打断的,其次,catch块会执行,就这些,再也没有其他动作了。已经输出的数据怎么办?使用这些数据的人或设备将收到一份不完整的(因而也是错误的)数据,却得不到任何有关这份数据是否完整的提示。对于有些系统来说,数据不完整可能比系统停止运行带来更大的损失

较为理想的处置办法是向输出设备写一些信息,声明数据的不完整性;另一种可能有效的办法是,先缓冲要输出的数据,准备好全部数据之后再一次性输出。

结论:全面考虑可能出现的异常以及这些异常对执行流程的影响。

改写后的代码


OutputStreamWriter out = ...

java.sql.Connection conn = ...

try {

Statement stat = conn.createStatement();

ResultSet rs = stat.executeQuery("select uid, name from user");

while (rs.next()){

out.println("ID:" + rs.getString("uid") + ",姓名: " + rs.getString("name"));

}

} catch(SQLException sqlex) {

out.println("警告:数据不完整");

throw new ApplicationException("读取数据时出现SQL错误", sqlex);

} catch(IOException ioex) {

throw new ApplicationException("写入数据时出现IO错误", ioex);

} finally {

if (conn != null) {

try {

conn.close();

} catch(SQLException sqlex2) {

System.err(this.getClass().getName() + ".mymethod -不能关闭数据库连接: "

+ sqlex2.toString());

}

}

if (out != null) {

try {

out.close();

} catch(IOException ioex2) {

System.err(this.getClass().getName() + ".mymethod -不能关闭输出文件"

+ ioex2.toString());

}

}

}

4. Java异常处理机制的优势与缺点

4.1 Java异常处理机制的优势

(1)
Java异常处理机制给错误进行了统一的分类,通过扩展Exception类或其子类来实现。从而避免了相同的错误可能在不同的方法中具有不同的错误信息。在不同的方法中出现相同的错误时,只需要throw 相同的异常对象即可。

(2)
可以精确的定位到导致程序出错的源代码位置,并获得详细的错误信息。通过异常类,可以给异常更为详细,对用户更为有用的错误信息,以便于用户进行跟踪和调试程序。

(3)
把正确的返回结果与错误信息分离,降低了程序的复杂度,调用者无需要对返回结果进行更多的了解。

(4)
强制调用者进行异常处理,提高程序的质量。当一个方法声明需要抛出一个异常时,那么调用者必须使用try…catch块对异常进行处理,当然调用者也可以让异常继续往上一层抛出。

4.2 使用checked异常带来的问题

4.2.1 导致太多的try…catch 代码

可能有很多checked异常对开发人员来说是无法合理地进行处理的,比如SQLException。而开发人员却不得不去进行try…catch。当开发人员对一个checked异常无法正确的处理时,通常是简单的把异常打印出来或者是干脆什么也不干。特别是对于新手来说,过多的checked异常让他感到无所适从。

try{

……

Statement s = con.createStatement();

……

} Catch(SQLException sqlEx){

sqlEx.PrintStackTrace();

}

或者

try{

……

Statement s = con.createStatement();

……

}Catch(SQLException sqlEx){

//什么也不干

}

4.2.2 导致许多难以理解的代码产生

当开发人员必须去捕获一个自己无法正确处理的checked异常,通常的是重新封装成一个新的异常后再抛出,这样做并没有为程序带来任何好处,反而使代码晚难以理解。

就像我们使用JDBC代码那样,需要处理非常多的try…catch.,真正有用的代码被包含在try…catch之内。使得理解这个方法变得困难起来。

4.2.3 导致异常被不断的封装成另一个异常后再抛出

public void methodA()throws ExceptionA{



throw new ExceptionA();

}

public void methodB()throws ExceptionB{

try{

methodA();



}catch(ExceptionA ex){

throw new ExceptionB(ex);

}

}

Public void methodC()throws ExceptinC{

try{

methodB();



}

catch(ExceptionB ex){

throw new ExceptionC(ex);

}

}

我们看到异常就这样一层层无休止的被封装和重新抛出。

4.2.4导致破坏接口方法

一个接口上的一个方法已被多个类使用,当为这个方法额外添加一个checked异常时,那么所有调用此方法的代码都需要修改。

可见上面这些问题都是因为调用者无法正确的处理checked异常时而被迫去捕获和处理,被迫封装后再重新抛出。这样十分不方便,并不能带来任何好处。在这种情况下通常使用unChecked异常。

chekced异常并不是无一是处,checked异常比传统编程的错误返回值要好用得多。通过编译器来确保正确的处理异常比通过返回值判断要好得多。

如果一个异常是致命的,不可恢复的。或者调用者去捕获它没有任何益处,使用unChecked异常。 在使用unChecked异常时,必须在在方法声明中详细的说明该方法可能会抛出的unChekced异常,由调用者自己去决定是否捕获unChecked异常。

如果一个异常是可以恢复的,可以被调用者正确处理的,使用checked异常。

5. 异常处理最佳实践

5.1 如何选择checked exception与Unchecked exception

采用checked exception还是Unchecked exception的时候,你要问自己一个问题,“如果这种异常一旦抛出,客户端能做怎样的补救?”

[原文:When deciding on checked exceptions vs. unchecked exceptions, ask yourself, "What action can the client code take when the exception occurs?"]

如果客户端可以通过其他的方法恢复异常,那么这种异常就是checked exception;如果客户端对出现的这种异常无能为力,那么这种异常就是Unchecked exception;从使用上讲,当异常出现的时候要做一些试图恢复它的动作而不要仅仅打印它的信息,总来的来说,看下表:

Client's reaction when exception happens
Exception type
Client code cannot do anything

Make it an unchecked exception

Client code will take some useful recovery action based on information in exception

Make it a checked exception

此外,尽量使用unchecked exception来处理编程错误:因为unchecked exception使客户端代码不用显示的处理它们,它们自己会在出现的地方挂起程序并打印出异常信息。Java API中提供了丰富的unchecked excetpion,譬如:NullPointerException , IllegalArgumentException
和 IllegalStateException等,因此我一般使用这些标准的异常类而不愿亲自创建新的异常类,这样使我的代码易于理解并避免的过多的消耗内存。

很有必要再重申一下,checked exception应该让客户端从中得到丰富的信息。要想让你的代码更加易读,请倾向于用unchecked excetpion来处理程序中的错误(Prefer unchecked exceptions for all programmatic errors)。

5.2 封装checked exception

让你抛出的checked exception升级到较高的层次。例如,不要让SQLException延伸到业务层,业务层并不需要(不关心)SQLException。你有两种方法来解决这种问题:

(1)转变SQLException为另外一个checked exception,如果客户端需要恢复这种异常的话

(2)转变SQLException为一个unchecked exception,如果客户端对这种异常无能为力的话

多数情况下,客户端代码都是对SQLException无能为力的,因此你要毫不犹豫的把它转变为一个unchecked exception,看看下边的代码:

public void dataAccessCode(){

try{

some code that throws SQLException

}catch(SQLException ex){

ex.printStacktrace();

}

}

上边的catch块仅仅打印异常信息,这是情有可原的,因为对于SQLException你还奢望客户端做些什么呢?但是这种就象什么事情都没发生一样的做法是不可取的,那么有没有另外一种更加可行的方法呢?

public void dataAccessCode(){

try{

some code that throws SQLException

}catch(SQLException ex){

throw new RuntimeException(ex);

}

}

上边的做法是把SQLException转换为RuntimeException,一旦SQLException被抛出,那么程序将抛出RuntimeException,此时程序被挂起并返回客户端异常信息。

当SQLException被抛出的时候,如果你有足够的信心恢复它,那么你也可以把它转换为一个有意义的checked exception, 但是我发现在大多时候抛出RuntimeException已经足够用了。

5.3 不要创建没有意义的异常

Try not to create new custom exceptions if they do not have useful information for client code。

看看下面的代码有什么问题?

public class DuplicateUsernameException extends Exception {

}

它除了有一个“意义明确”的名字以外没有任何有用的信息了。不要忘记Exception跟其他的Java类一样,客户端可以调用其中的方法来得到更多的信息。

我们可以为其添加一些必要的方法,如下:

public class DuplicateUsernameException extends Exception {

public DuplicateUsernameException (String username){....}

public String requestedUsername(){...}

public String[] availableNames(){...}

}

在新的代码中有两个有用的方法:reqeuestedUsername(),客户但可以通过它得到请求的名称;availableNames(),客户端可以通过它得到一组有用的usernames。这样客户端在得到其返回的信息来明确自己的操作失败的原因。但是如果你不想添加更多的信息,那么你可以抛出一个标准的Exception:

throw new Exception("Username already taken");

如果你认为客户端并不想作过多的操作而仅仅想看到异常信息,你可以抛出一个unchecked exception:

throw new RuntimeException("Username already taken");

5.4 不要使用异常来控制流程

Never use exceptions for flow control。

下边代码中,MaximumCountReachedException被用于控制流程:

public void useExceptionsForFlowControl() {

try {

while (true) {

increaseCount();

}

} catch (MaximumCountReachedException ex) {

}

//Continue execution

}

public void increaseCount() throws MaximumCountReachedException {

if (count >= 5000)

throw new MaximumCountReachedException();

}

上边的useExceptionsForFlowControl()用一个无限循环来增加count直到抛出异常,这种做法并没有说让代码不易读,但是它是程序执行效率降低。

记住,只在需要抛出异常的地方进行异常处理。

5.5 不要忽略异常

当有异常被抛出的时候,如果你不想恢复它,那么你要毫不犹豫的将其转换为unchecked exception,而不是用一个空的catch块或者什么也不做来忽略它,以至于从表面来看象是什么也没有发生一样。

5.6 不要捕获顶层的Exception

unchecked exception都是RuntimeException的子类,RuntimeException又继承Exception,因此,如果单纯的捕获Exception,那么你同样也捕获了RuntimeException,如下代码:

try{

..

}catch(Exception ex){

}

一旦你写出了上边的代码(注意catch块是空的),它将捕获所有的异常,包括unchecked exception。

5.7 抛出异常的时机与类型

5.7.1 什么时候抛出异常

这个问题涉及到服务类。我想异常本身这个字解释了某些东西,异常就是我们认为在正常情况下不可能发生的问题,并且服务代码不知道如何去处理。譬如说我做一个监控程序,需要用压缩卡提供的API去初始化所有的板卡,API提供的是boolean型的返回值,但我把这个API变成抛出一个异常,因为除非特殊原因,我不认为会发生初始化失败的情况,当然更不知道怎样去处理这个问题。又譬如Hibernate里面的LoadObject使用没有发现这个对象存在,那Hibernate也是认为不可能的,除非其他代码直接删除了数据库里面的记录,那么也需要抛出异常。当然Hibernate本身也不知道如何处理这种情况。

但是如果发生的情况是可以预期的,那我不认为应该抛出例外。象这个userExist的情况(定义大量的Exception类,所有这些Exception类都不意味着程序出现了异常或者错误,只是代表非主事件流的发生的,用来进行那些分支流程的流程控制的。例如你往权限系统中增加一个用户,应该定义1个异常类,UserExistedException,抛出这个异常不代表你插入动作失败,只说明你碰到一个分支流程,留待后面的catch中来处理这个分支流程。传统的程序员会写一个if
else来处理,而一个合格的OOP程序员应该有意识的使用try catch 方式来区分主事件流和n个分支流程的处理,通过try catch,而不是if else来从代码上把不同的事件流隔离开来进行分别的代码撰写。),我认为应该在前面已经分流,应该首先判断这个用户是否存在,if(userExists()),然后进行处理,而不应当抛出例外。以及login应当返回true或者false。也就是说,这些属于程序的正常流程,而不是例外,不是异常。把例外作为正常程序流程的控制机制,只不过是把服务代码中的if转移到客户代码去,没有减少任何需要处理的代码,反而增加了系统的负担(生成例外栈)。

还有抛出异常的情况是违反方法的先决条件,每一个方法都有自己的先决条件和后置条件,方法只有在正确的前提下才能执行达到一个正确的后果,(所谓类的不变量)。譬如你去存取一个数组的某一个元素,这个存取方法有一个前提条件,就是你的索引应当落入它的最大下标和最小下标之间,不然就应当抛出一个例外。

5.7.2 抛出checked还是unchecked异常

这个问题涉及到客户类,视于客户代码是否能够根据这个例外进行合理的处理。如果客户代码根本就不知道如何处理这个例外,应当把它作为一个unchecked例外,例如上面下标的问题,客户代码用一个不合法的下标来存取数组,那么抛出一个checked例外以后,客户代码是+1还是-1?显然根本就不可能做出“合理的”处理,客户既然不能处理,还要强制它去处理,那么就是捕获,打印了事,没有增加任何价值。但是如果是客户可以处理的,或者可以选择不同的方式处理的,那么就可能需要用checked,但我发现很少有这样的情况。对于类似于RemoteException或者SQLException这些Exception,我一般都转换为具体的业务Exception,而我所有的业务Exception都是RuntimeException。

所以我的观点是,是否抛出例外就是服务代码是否进行合理的处理,抛出什么类型的例外就是客户代码是否能够合理的处理。

6. 如何设计异常类

在设计一个新的异常类时,首先看看是否真正的需要这个异常类。一般情况下尽量不要去设计新的异常类,而是尽量使用java中已经存在的异常类。

不管是新的异常是chekced异常还是unChecked异常,我们都必须考虑异常的嵌套问题。

public void methodA()throws ExceptionA{



throw new ExceptionA();

}

方法methodA声明会抛出ExceptionA。

public void methodB()throws ExceptionB

methodB声明会抛出ExceptionB,当在methodB方法中调用methodA时,ExceptionA是无法处理的,所以ExceptionA应该继续往上抛出。一个办法是把methodB声明会抛出ExceptionA,但这样已经改变了MethodB的方法签名,一旦改变,则所有调用methodB的方法都要进行改变。

另一个办法是把ExceptionA封装成ExceptionB,然后再抛出。如果我们不把ExceptionA封装在ExceptionB中,就丢失了根异常信息,使得无法跟踪异常的原始出处。

public void methodB()throws ExceptionB{

try{

methodA();

……

}catch(ExceptionA ex){

throw new ExceptionB(ex);

}

}

如上面的代码中,ExceptionB嵌套一个ExceptionA.我们暂且把ExceptionA称为“起因异常”,因为ExceptionA导致了ExceptionB的产生,这样才不使异常信息丢失。

所以我们在定义一个新的异常类时,必须提供这样一个可以包含嵌套异常的构造函数,并有一个私有成员来保存这个“起因异常”。

public Class ExceptionB extends Exception{

private Throwable cause;

public ExceptionB(String msg, Throwable ex){

super(msg);

this.cause = ex;

}

public ExceptionB(String msg){

super(msg);

}

public ExceptionB(Throwable ex){

this.cause = ex;

}

}

当然,我们在调用printStackTrace方法时,需要把所有的“起因异常”的信息也同时打印出来,所以我们需要覆写printStackTrace方法来显示全部的异常栈跟踪,包括嵌套异常的栈跟踪。

public void printStackTrace(PrintStrean ps){

if(cause == null){

super.printStackTrace(ps);

}else{

ps.println(this);

cause.printStackTrace(ps);

}

}

一个完整的支持嵌套的checked异常类源码如下,我们在这里暂且把它叫做NestedException。

public NestedException extends Exception{

private Throwable cause;

public NestedException (String msg){

super(msg);

}

public NestedException(String msg, Throwable ex){

super(msg);

This.cause = ex;

}

public Throwable getCause(){

return (this.cause == null ? this :this.cause);

}

public getMessage(){

String message = super.getMessage();

Throwable cause = getCause();

if(cause != null){

message = message + “;nested Exception is ” + cause;

}

return message;

}

public void printStackTrace(PrintStream ps){

if(getCause == null){

super.printStackTrace(ps);

}else{

ps.println(this);

getCause().printStackTrace(ps);

}

}

public void printStackTrace(PrintWrite pw){

if(getCause() == null){

super.printStackTrace(pw);

}

else{

pw.println(this);

getCause().printStackTrace(pw);

}

}

public void printStackTrace(){

printStackTrace(System.error);

}

}

同样要设计一个unChecked异常类也与上面一样,只是需要继承RuntimeException。

7. 如何记录异常信息

作为一个大型的应用系统都需要用日志文件来记录系统的运行,以便于跟踪和记录系统的运行情况,系统发生的异常理所当然的需要记录在日志系统中。

public String getPassword(String userId)throws NoSuchUserException{

UserInfo user = userDao.queryUserById(userId);

If(user == null){

Logger.info(“找不到该用户信息,userId=”+userId);

throw new NoSuchUserException(“找不到该用户信息,userId=”+userId);

}else{

return user.getPassword();

}

}

public void sendUserPassword(String userId)throws Exception {

UserInfo user = null;

try{

user = getPassword(userId);

//……..

sendMail();

//

}catch(NoSuchUserException ex)(

logger.error(“找不到该用户信息:”+userId+ex);

throw new Exception(ex);

}

}

我们注意到,一个错误被记录了两次,在错误的起源位置我们仅是以info级别进行记录。而在sendUserPassword方法中,我们还把整个异常信息都记录了。

笔者曾看到很多项目是这样记录异常的,只有遇到异常就把整个异常全部记录下,如果一个异常被不断的封装抛出多次,那么就被记录了多次。那么异常倒底该在什么地方被记录?--异常应该在最初产生的位置记录!

如果必须捕获一个无法正确处理的异常,仅仅是把它封装成另外一种异常往上抛出,不必再次把已经被记录过的异常再次记录。

如果捕获到一个异常,但是这个异常是可以处理的,则无需要记录异常。

public Date getDate(String str){

Date applyDate = null;

SimpleDateFormat format = new SimpleDateFormat(“MM/dd/yyyy”);

try{

applyDate = format.parse(applyDateStr);

}

catch(ParseException ex){

//忽略,当格式错误时,返回null

}

return applyDate;

}

捕获到一个未记录过的异常或外部系统异常时,应该记录异常的详细信息

try{

……

String sql=”select * from userinfo”;

Statement s = con.createStatement();

}Catch(SQLException sqlEx){

Logger.error(“sql执行错误”+sql+sqlEx);

}

究竟在哪里记录异常信息,及怎么记录异常信息,可能是见仁见智的问题了。甚至有些系统让异常类一记录异常。当产生一个新异常对象时,异常信息就被自动记录。

public class BusinessException extends Exception {

private void logTrace() {

StringBuffer buffer=new StringBuffer();

buffer.append("Business Error in Class: ");

buffer.append(getClassName());

buffer.append(",method: ");

buffer.append(getMethodName());

buffer.append(",messsage: ");

buffer.append(this.getMessage());

logger.error(buffer.toString());

}

public BusinessException(String s) {

super(s);

logTrace ();

}

}

这似乎看起来是十分美妙的,其实必然导致了异常被重复记录。同时违反了“类的职责分配原则”,是一种不好的设计。记录异常不属于异常类的行为,记录异常应该由专门的日志系统去做,并且异常的记录信息是不断变化的,我们在记录异常同应该给更丰富些的信息,以利于我们能够根据异常信息找到问题的根源,以解决问题。

虽然我们对记录异常讨论了很多,过多的强调这些反而使开发人员更为疑惑,一种好的方式是为系统提供一个异常处理框架,由框架来决定是否记录异常和怎么记录异常,而不是由普通程序员去决定。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: