异常处理-最佳实践
在Google学术上搜到一篇有意思,但也很有争议的文章,题为《Best Practices for Exception Handling》 O'Reilly Media。
checked和unchecked的含义其实就是是否需要进行特定的检查,即“当一个exception发生时,是否需要用catch块将其接收消化或者用throws出去让别人接收处理”。如果不用特意检查,那么就属于unchecked类型的异常,如果需要检查就是checked类型的异常。
Java的checked类型异常是争议比较多的东西。因为在常见的OO语言中,Java是唯一拥有checked类型异常的语言,而C++和C#都是只有unchecked类型的异常而没有checked类型的异常。
在java中,耦合的评价标准不只包括数据接口,还包括接口间的异常传递关系。请看文中给出的一个例子:
public List getAllAccounts() throws
FileNotFoundException, SQLException{
...
}
可以看到getAllAccounts()方法向其调用者强制要求处理两个异常,分别为FileNotFoundException和SQLException,这就产生了一种exception上的耦合。
- 什么时候该使用checked类型的异常,什么时候又该使用unchecked类型的异常呢?
作者认为,当API的调用者可以使用一系列步骤恢复程序的运行状态时,就抛出checked异常给API的调用者处理。但是,如果这个异常是API的调用者无法恢复的,那么就应该以unchecked的方式抛出。
- 如何保护接口的封装性呢?
作者的建议是不要让有特定类型的异常上升到更高的层次上。比如不应该让SQLException上升到业务逻辑层,而应该在数据接口层就将其处理掉。对于SQLException的处理,文章的作者认为可以这样处理:
(1)将SQLException转换成其他的checked异常。这种情况适用于客户端代码(即调用API的代码)希望并且有能力处理这个SQL异常。 (2)将SQLException转换成unchecked异常。这种情况适用于客户端对SQL异常无能为力的情况。
在这两种方式之间,文章的作者更倾向于后者,即封装成unchecked异常。因为,封装成unchecked异常抛出后,客户端代码不需要写特定的catch块进行SQL处理,并且封装成unchecked异常后,可以让系统将这个错误记录在日志中。另外,如果catch块中希望知道这个异常发生的原因,可以调用exception对象的getCause()方法来获得。文中,作者还提到大部分的时候,使用第二种方式便可以取得令人满意的效果。
- 不要轻易新建自己的异常类
当你自己定义的异常类不提供任何可以获取更多信息的方法时,不要新建自己的类,使用标准异常类足矣。如果要定义自己的异常类,那么可以在自己的异常类中增加一些可以提供辅助信息的方法,如文中作者的示例:
public class DuplicateUsernameException
extends Exception {
public DuplicateUsernameException
(String username){....}
public String requestedUsername(){...}
public String[] availableNames(){...}
}
可以看到,在定制的DuplicateUsernameException中增加了获取请求的用户名以及可用用户名的方法。DuplicateUsername意味着重复的用户名,即请求的这个用户名已经被别人使用过了。那么,在这个情况下,可以为用户提供几个与其所请求的用户名相近的几个可用的用户名作为建议。
但是,提供建议的用户名应该属于业务逻辑的内容,不应该参杂在异常处理的类中来完成。异常处理应该专注于恢复异常、记录异常以及显示提示信息。 但是像文中作者提出的提供建议用户名的方式,如果可以帮助从这个异常中恢复出来,那么也属于异常处理的范畴。这就要看业务块的划分了。究竟是“完成一次注册”算是一个用户操作块,还是“成功注册”算是一个用户操作块。如果是前者,那么提示建议用户名的方法就可以放在异常处理的代码块中。如果是后者,那么“注册失败”就和“注册成功”是同等的操作块,而“提示建议用户名”就是另一个与其相同层次上的操作,就不应该放到异常处理中了。
所以,业务块、操作块的粒度划分也会影响到异常处理的职责设定。
- 为你的异常撰写说明
在Java中,可以用Javadoc的 @throws 标签撰写异常的说明。但是文中作者更推荐用单元测试的方式来说明一个异常。如:
public void testIndexOutOfBoundsException() {
ArrayList blankList = new ArrayList();
try {
blankList.get(10);
fail("Should raise an IndexOutOfBoundsException");
} catch (IndexOutOfBoundsException success) {}
}
通过撰写单元测试的方式来说明一个异常有两个好处,一是可以让调用API的人员更清楚这个异常产生的机制,另一个原因是可以使用这个单元测试来测试此异常,以增加代码的鲁棒性。
使用Exception的最佳范例
做好代码的清理工作
如果你的代码中使用了一些资源,如数据库连接、网络连接等,那么应该及时地关闭这些连接。关闭连接的代码应该放在finally块中,这样可以保证不论是否有异常发生,资源连接都可以进行关闭。如文中给出的范例:
public void dataAccessCode(){
Connection conn = null;
try{
conn = getConnection();
..some code that throws SQLException
}catch(SQLException ex){
ex.printStacktrace();
} finally{
DBUtil.closeConnection(conn);
}
}
class DBUtil{
public static void closeConnection
(Connection conn){
try{
conn.close();
} catch(SQLException ex){
logger.error("Cannot close connection");
throw new RuntimeException(ex);
}
}
}
不要使用exception来进行流程控制
public void useExceptionsForFlowControl() {
try {
while (true) {
increaseCount();
}
} catch (MaximumCountReachedException ex) {
}
//Continue execution
}
public void increaseCount()
throws MaximumCountReachedException {
if (count >= 5000)
throw new MaximumCountReachedException();
}
可以看到例子中使用了 MaximumCountReachedException 来处理计数达到一定数量后的情况。这样做有两个坏处:一是使代码不易理解,二是将异常处理混入到了算法逻辑中。异常处理应该只使用在需要处理异常的情况下,而不应该越界到业务逻辑的范围内。
不要压制(suppress)或忽略exception
当一个API要求你处理某个exception时,说明它希望你能采取一些措施以应对这个异常。如果你也对这个异常束手无策的话,不要只是catch它然后让它在catch块中被忽略掉,而是至少将它转换成一个unchecked异常,然后抛出。
不要捕获底层的异常
不管是unchecked类型的异常还是checked类型的异常,都是继承自Exception这个基类。如果在代码中,直接catch这个基类的话,那么所有的unchecked异常也都被捕获了。另外,如果在捕获Exception基类时,不做任何处理,反而会影响到unchecked异常的处理流程,因为作为Exception的基类,unchecked类型的异常也被捕获了。
一个异常不要进行多次记录
一个异常如果进行多次记录会让程序员在追踪异常线索时很混乱。