关于异常处理最主要的问题是什么时间、怎么使用,在本文将含括一些关于异常处理的最佳实践方案。
异常的本质(这里有三种不同的情况会产生并抛出异常):
- 程序错误:这种异常时由于程序的错误而产生的(例如,N
ullPointerException
andIllegalArgumentException)。
- 代码调用错误:客户端代码试图违反API协议调用接口。如果异常能提供一些有用的信息,客户端可以采取一些替代的行为。例如:解析一个不符合格式要求的XML文档而抛出的异常。如果这个异常能包含一些有用的信息(例如xml文档的那个位置出错),那么客户端就可以利用这些信息来采取回复步骤了。
- 资源故障错误:例如系统内存不足,网络连接失败。客户端可以在一段时间后重试操作,或者记录资源失败,停止服务。
Java中的异常种类:
在java中定义了两种异常:
- Checked exceptions: 继承于exception类的异常是checked exceptions, 客户端代码必须处理从API中抛出的此类异常,可以用catch块来处理,也可以用throw把他抛给外面来处理。
- Unchecked exceptions:继承于RuntimeException类的异常是Unchecked exceptions,但是所有继承于RuntimeException的类都得到了特殊对待,将不要求客户代码必须处理他们,因此他们被叫做Unchecked exceptions。
下图展示了NullPointerException的继承关系:他继承RuntimeException,因此是一个Unchecked exceptions.
最佳接口设计实践
- 当你要决定使用哪种异常的时候,你可以先问问自己“当这种异常发生时,客户端代码可以采取什么行为?”
如果客户端可以根据异常采取一些替代行为来恢复系统运行,那么就用checked exception。反之,如果客户端对异常无能为力(记录日志也算没有任何用处, 这里说的有用措施是可以从异常中恢复系统运行),那么就使用unchecked exception吧。
- 对异常进行封装。
从来不要让具体实现中的特定异常被抛到外层,例如不要把SQLException从DAO层抛到业务逻辑层,因为业务逻辑不需要知道具体的SQLException是什么,这时我们有两种选择:
-
- 如果客户端逻辑需要这些错误信息,那么把SQLException转换成另一种checked exception,即封装。
- 如果客户端逻辑不需要这些,那么把SQLException转换成另一种unchecked exception,即封装。
大多数时间里,客户端不能对这些SQLException做任何事情, 所以尽量把它们转换成unchecked exceptions. 看看下面两段代码:
- public void dataAccessCode(){
- try{
- ..some code that throws SQLException
- }catch(SQLException ex){
- ex.printStacktrace();
- }
- }
- public void dataAccessCode(){
- try{
- ..some code that throws SQLException
- }catch(SQLException ex){
- throw new RuntimeException(ex);
- }
- }
- public void dataAccessCode(){ try{ ..some code that throws SQLException }catch(SQLException ex){ ex.printStacktrace(); } } public void dataAccessCode(){ try{ ..some code that throws SQLException }catch(SQLException ex){ throw new RuntimeException(ex); } }
如果你确定在业务逻辑层能根据DAO的异常做一些恢复操作,那么你可以把它们转换成更有意义的checked exception。
- 不要试图去创建任何对客户端来说没有有用信息的自定义异常, 参照下面两段代码:
- public class DuplicateUsernameException
- extends Exception {}
- public class DuplicateUsernameException
- extends Exception {
- public DuplicateUsernameException
- (String username){....}
- public String requestedUsername(){...}
- public String[] availableNames(){...}
- }
public class DuplicateUsernameException extends Exception {} public class DuplicateUsernameException extends Exception { public DuplicateUsernameException (String username){....} public String requestedUsername(){...} public String[] availableNames(){...} }
异常使用的最佳实践
- Always clean up after yourself
如果你正在使用数据库连接或者网络连接时,一定要记住释放资源,及时你调用的接口只抛出unckecked exception, 也一定要在使用之后利用try - 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);
- }
- }
- }
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); } } }
- 从来不要用异常来做流程控制。
生成/收集内存stack traces信息是一件代价高昂的事情,stack trace是用来调试的。在try-catch块时他会自动收集stacktrace信息,降低运行速度,而异常处理只有在异常情况的时候才使用。
- public void useExceptionsForFlowControl() {
- try {
- while (true) {
- increaseCount();
- }
- } catch (MaximumCountReachedException ex) {
- }
- //Continue execution
- }
- public void increaseCount()
- throws MaximumCountReachedException {
- if (count >= 5000)
- throw new MaximumCountReachedException();
- }
public void useExceptionsForFlowControl() { try { while (true) { increaseCount(); } } catch (MaximumCountReachedException ex) { } //Continue execution } public void increaseCount() throws MaximumCountReachedException { if (count >= 5000) throw new MaximumCountReachedException(); }
- 不要压制/忽略/吃掉异常
当一个checked exception从一个API被抛出时,它是在试图告诉你要采取一些必要措施来处理他,如果异常对你来说没有任何意义,那么就把他封装成unchecked exception并抛给上一层,千万不要用catch块{}来忽略/吃掉它,当作什么事都没发生过。
- 不要捕捉顶层的异常
事实上,Unchecked exceptions 继承于 RuntimeException 类,而它又是继承Exception类,如果捕捉
Exception
类的话,同时也捕捉了RuntimeException,这样也同时忽略我们本来要抛给上层的Unchecked exceptions。
- try{
- ..
- }catch(Exception ex){
- }
try{ .. }catch(Exception ex){ }
- 同一个异常只记录一次(Log exceptions just once)
同一个异常的stack trace被记录多次会给程序员检查原始异常源的日志stack trace带来不必要的麻烦/困惑,而且会带来更大的IO量,所以只记录一次。