在咱们深刻了解异常处理最佳实践的深层概念以前,让咱们从一个最重要的概念开始,那就是理解在JAVA中有三种通常类型的可抛类: 检查性异常(checked exceptions)、非检查性异常(unchecked Exceptions) 和 错误(errors)。java
检查性异常(checked exceptions) 是必须在在方法的throws
子句中声明的异常。它们扩展了异常,旨在成为一种“在你面前”的异常类型。JAVA但愿你可以处理它们,由于它们以某种方式依赖于程序以外的外部因素。检查的异常表示在正常系统操做期间可能发生的预期问题。 当你尝试经过网络或文件系统使用外部系统时,一般会发生这些异常。 大多数状况下,对检查性异常的正确响应应该是稍后重试,或者提示用户修改其输入。
非检查性异常(unchecked Exceptions) 是不须要在throws子句中声明的异常。 因为程序错误,JVM并不会强制你处理它们,由于它们大多数是在运行时生成的。 它们扩展了RuntimeException
。 最多见的例子是NullPointerException
[至关可怕..是否是?]。 未经检查的异常可能不该该重试,正确的操做一般应该是什么都不作,并让它从你的方法和执行堆栈中出来。 在高层次的执行中,应该记录这种类型的异常。
错误(errors) 是严重的运行时环境问题,几乎确定没法恢复。 例如OutOfMemoryError
,LinkageError
和StackOverflowError
, 它们一般会让程序崩溃或程序的一部分。 只有良好的日志练习才能帮助你肯定错误的确切缘由。数据库
任什么时候候,当用户以为他出于某种缘由想要使用本身的特定于应用程序的异常时,他能够建立一个新的类来适当的扩展超类(主要是它的Exception.java)并开始在适当的地方使用它。 这些用户定义的异常能够以两种方式使用:
1) 当应用程序出现问题时,直接抛出自定义异常服务器
throw new DaoObjectNotFoundException("Couldn't find dao with id " + id);
2) 或者将自定义异常中的原始异常包装并抛出网络
catch (NoSuchMethodException e) { throw new DaoObjectNotFoundException("Couldn't find dao with id " + id, e); }
包装异常能够经过添加本身的消息/上下文信息来为用户提供额外信息,同时仍保留原始异常的堆栈跟踪和消息。 它还容许你隐藏代码的实现细节,这是封装异常的最重要缘由。 测试
如今让咱们开始探索遵循行业聪明的异常处理的最佳实践。spa
catch (NoSuchMethodException e) { return null; }
public void foo() throws Exception { //错误方式 }
必定要避免出现上面的代码示例。 它简单地破坏了检查性异常的整个目的。 声明你的方法可能抛出的具体检查性异常。 若是只有太多这样的检查性异常,你应该把它们包装在你本身的异常中,并在异常消息中添加信息。 若是可能的话,你也能够考虑代码重构。线程
public void foo() throws SpecificException1, SpecificException2 { //正确方式 }
try { someMethod(); } catch (Exception e) { //错误方式 LOGGER.error("method has failed", e); }
捕获异常的问题是,若是稍后调用的方法为其方法声明添加了新的检查性异常,则开发人员的意图是应该处理具体的新异常。 若是你的代码只是捕获异常(或Throwable),你永远不会知道这个变化,以及你的代码如今是错误的,而且可能会在运行时的任什么时候候中断。debug
这是一个更严重的麻烦。 由于java错误也是Throwable的子类。 错误是JVM自己没法处理的不可逆转的条件。 对于某些JVM的实现,JVM可能实际上甚至不会在错误上调用catch子句。日志
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " + e.getMessage()); //错误方式 }
这破坏了原始异常的堆栈跟踪,而且始终是错误的。 正确的作法是:code
catch (NoSuchMethodException e) { throw new MyServiceException("Some information: " , e); //正确方式 }
catch (NoSuchMethodException e) { //错误方式 LOGGER.error("Some information", e); throw e; }
正如在上面的示例代码中,记录和抛出异常会在日志文件中产生多条日志消息,代码中存在单个问题,而且让尝试挖掘日志的工程师生活得很糟糕。
try { someMethod(); //Throws exceptionOne } finally { cleanUp(); //若是finally还抛出异常,那么exceptionOne将永远丢失 }
只要cleanUp()永远不会抛出任何异常,上面的代码没有问题。 可是若是someMethod()抛出一个异常,而且在finally块中,cleanUp()也抛出另外一个异常,那么程序只会把第二个异常抛出来,原来的第一个异常(正确的缘由)将永远丢失。 若是你在finally块中调用的代码可能会引起异常,请确保你要么处理它,要么将其记录下来。 永远不要让它从finally块中抛出来。
catch (NoSuchMethodException e) { throw e; //避免这种状况,由于它没有任何帮助 }
这是最重要的概念。 不要为了捕捉异常而捕捉,只有在想要处理异常时才捕捉异常,或者但愿在该异常中提供其余上下文信息。 若是你不能在catch块中处理它,那么最好的建议就是不要只为了从新抛出它而捕获它。
完成代码后,切勿忽略printStackTrace()。 你的同事可能会最终获得这些堆栈,而且对于如何处理它彻底没有任何知识,由于它不会附加任何上下文信息。
try { someMethod(); //Method 2 } finally { cleanUp(); //do cleanup here }
这也是一个很好的作法。 若是在你的方法中你正在访问Method 2,而Method 2抛出一些你不想在method 1中处理的异常,可是仍然但愿在发生异常时进行一些清理,而后在finally块中进行清理。 不要使用catch块。
这多是关于异常处理最著名的原则。 它基本上说,你应该尽快抛出(throw)异常,并尽量晚地捕获(catch)它。 你应该等到你有足够的信息来妥善处理它。
这个原则隐含地说,你将更有可能把它放在低级方法中,在那里你将检查单个值是否为空或不适合。 并且你会让异常堆栈跟踪上升好几个级别,直到达到足够的抽象级别才能处理问题。
若是你正在使用数据库链接或网络链接等资源,请确保清除它们。 若是你正在调用的API仅使用非检查性异常,则仍应使用try-finally块来清理资源。 在try模块里面访问资源,在finally里面最后关闭资源。 即便在访问资源时发生任何异常,资源也会优雅地关闭。
相关性对于保持应用程序清洁很是重要。 一种尝试读取文件的方法; 若是抛出NullPointerException,那么它不会给用户任何相关的信息。 相反,若是这种异常被包裹在自定义异常中,则会更好。 NoSuchFileFoundException则对该方法的用户更有用。
咱们已经阅读过不少次,但有时咱们仍是会在项目中看到开发人员尝试为应用程序逻辑而使用异常的代码。 永远不要这样作。 它使代码很难阅读,理解和丑陋。
始终要在很是早的阶段验证用户输入,甚至在达到实际controller以前。 它将帮助你把核心应用程序逻辑中的异常处理代码量降到最低。 若是用户输入出现错误,它还能够帮助你使与应用程序保持一致。
例如:若是在用户注册应用程序中,你遵循如下逻辑:
1)验证用户
2)插入用户
3)验证地址
4)插入地址
5)若是出问题回滚一切
这是很是不正确的作法。 它会使数据库在各类状况下处于不一致的状态。 首先验证全部内容,而后将用户数据置于dao层并进行数据库更新。 正确的作法是:
1)验证用户
2)验证地址
3)插入用户
4)插入地址
5)若是问题回滚一切
LOGGER.debug("Using cache sector A"); LOGGER.debug("Using retry sector B");
不要这样作。
对多个LOGGER.debug()调用使用多行日志消息可能在你的测试用例中看起来不错,可是当它在具备400个并行运行的线程的应用程序服务器的日志文件中显示时,全部转储信息都是相同的日志文件,你的两个日志消息最终可能会在日志文件中间隔1000行,即便它们出如今代码的后续行中。
像这样作:
LOGGER.debug("Using cache sector A, using retry sector B");
有用且信息丰富的异常消息和堆栈跟踪也很是重要。 若是你的日志不能肯定任何事情(有效内容不全或很难肯定问题缘由),
那要日志有什么用? 这类的日志只是你代码中的装饰品。
while (true) { try { Thread.sleep(100000); } catch (InterruptedException e) {} //别这样作 doSomethingCool(); }
InterruptedException是你的代码的一个提示,它应该中止它正在作的事情。 线程中断的一些常见用例是active事务超时或线程池关闭。 你的代码应该尽最大努力完成它正在作的事情,而且完成当前的执行线程,而不是忽略InterruptedException。 因此要纠正上面的例子:
while (true) { try { Thread.sleep(100000); } catch (InterruptedException e) { break; } } doSomethingCool();
在你的代码中有100个相似的catch块是没有用的。 它增长代码的重复性并且没有任何的帮助。 对这种状况要使用模板方法。
例如,下面的代码尝试关闭数据库链接。
class DBUtil{ public static void closeConnection(Connection conn){ try{ conn.close(); } catch(Exception ex){ //Log Exception - Cannot close connection } } }
这种类型的方法将在你的应用程序的成千上万个地方使用。 不要把这块代码放的处处都是,而是定义顶层的方法,并在下层的任何地方使用它:
public void dataAccessCode() { Connection conn = null; try{ conn = getConnection(); .... } finally{ DBUtil.closeConnection(conn); } }
把注释(javadoc)运行时可能抛出的全部异常做为一种习惯。
也要尽量包括可行的方案,用户应该关注这些异常发生的状况。
这就是我如今所想的。 若是你发现任何遗漏或你与个人观点不一致,请发表评论。 我会很乐意讨论。