2013年2月2日土曜日

异常处理最佳实践


作者:Gunjan Doshi 译者:刘晓日
异常处理通常都会遇到这样一个问题:何时以及如何使用异常。本文主要介绍异常处理的最佳实践,当然也会针对目前对检查性异常做一些总结与归纳。
作为开发人员,我们都希望能编写既具高质量又能解决问题的代码,不幸的是,异常对代码质量总是会起到负面的影响。没有哪个程序员愿意一味的接受这种负面的影响,所以我们总是寻找各种方法避免异常处理。我曾经看过优秀的程序员这样处理异常:
public void consumeAndForgetAllExceptions(){
    try {
        ...some code that throws exceptions
    } catch (Exception ex){
        ex.printStacktrace();
    }
}
这段代码有什么问题吗?
只是,当有异常被抛出,当前执行程序被挂起,控制权移交到catch块中。catch块除了将异常捕获什么也没做,然后catch块后面的程序继续执行,就好像什么都没发生一样。
下面这种方式怎么样?
public void someMethod() throws Exception{
}
这就是一个空方法,方法体内根本一行代码都没有,怎么还要抛出异常呢?在java里面,你确实可以这样干。我就遇到过在一段很简单的代码中声明抛出异常,却没有任何一行代码会引发异常。当我询问程序员为何要这么做的时候,他这样回答我:“我知道这样使API看起来很糟糕,但是我一直都是这样做的,而且这样做也奏效。”
C++社区花了几年时间研究要怎么处理异常,然而关于异常处理的讨论在java社区也开始了,越来越多的java程序员正在与异常处理做斗争。如果异常使用不当,会造成程序执行缓慢,因为创建和捕获异常需要占用内存和CPU。过度使用异常,一方面会造成程序的可读性极差,另一方面会给调用者造成不必要的麻烦。编写代码时,很可能像上面两个例子一样,直接将异常抛出或者忽略。
The Nature of Exceptions
总的来说,三种情况会引发异常:
  • 运行时异常:这种异常,是由程序运行时错误引发的,比如NullPointerException、IllegalArgumentException 。对于这种运行时错误,我们无能为力,做不了任何处理。
  • 代码错误引发的异常:调用者编码时,违反API的约定引发的异常。如果在异常中包含着重要的信息,那么调用者可以采取一些针对该异常的补救方法。比如在解析XML文档的时候,因格式不正确引发异常,异常中会记录引发异常的位置,这样,编写代码时,就可以利用它采取补救措施。
  • 资源错误引发的异常:当请求资源失败时,引发的异常。比如内存溢出或者网络连接失败等。针对这种异常的处理要权衡需求,可以超时重新发送请求,也可以记录下失败的资源后停止应用程序。
Types of Exceptions in Java
java中定义了两类异常:
  • 检查性异常:检查性异常继承自Exception,调用者必须在catch块中捕获这类异常,或者将异常抛到上层。
  • 非检查性异常:RuntimeException 也是继承自Exception,所有继承自RuntimeException的异常,都不需要进行处理,所以叫做非检查型异常。
通过举例的方式,图一展现了NullPointerException的继承关系。
enter image description here
图一:异常继承关系
图中NullPointerException继承自RunTimeException,所以是非检查性异常。
目前非检查性异常很少使用,更多的是使用检查性异常。最近java社区中关于检查性异常及其价值的争论异常火热。这场争论源自于java是第一个使用检查性异常的主流面向对象编程语言。C++和C#中没有检查性异常一说,全部都是非检查性异常。
检查性异常强制要求调用者捕获异常,或向上层抛出异常。如果调用者,无法对检查性异常做出有效的处理,那么这种强制性捕获或抛出的约定就会变成一种负担。编程人员可能采取偷懒的方式使用空白的catch块将异常忽略,或者干脆直接抛出。事实上,这已经造成了调用者的负担。
检查性异常还违反封装性原则。看一下下面这段代码:
public List getAllAccounts() throws
    FileNotFoundException, SQLException{
    ...
}
getAllAccounts()方法抛出两种检查性异常。尽管你还不知道getAllAccounts()中调用哪个文件或数据库失败,或是不支持文件系统或数据库逻辑,但是调用getAllAccounts()时必须显式的处理这两种异常。所以,检查性异常迫使方法和它的调用者间保持着高度的耦合。
Best Practices for Designing the API
前面已经说了很多,现在我们来看看如何正确设计异常处理的API。
1、当你不知道应该使用检查性异常还是非检查型异常时,不妨这样问自己:当捕获到异常时,通过编码我能做些什么?
如果异常发生时,通过编码的方式补救异常发生的情况,那么它就是检查性异常。当然如果无法通过编码方式采取任何有用的处理,那么就是非检查性异常。这里的有用性,是指能减少异常发生带来的“损失”,而不是简单记录一下异常信息。总结如下:
  • 调用者什么也做不了,则使用非检查型异常。
  • 可根据异常携带的信息做出进一步处理,则使用检查性异常。
而且,运行时错误作为非检查性异常的优点在于:非检查性异常不会强制调用者显式的处理异常。可以在需要的时候捕获非检查性异常,没必要时就不进行捕获,记录一下就好。 (Moreover, prefer unchecked exceptions for all programming errors: unchecked exceptions have the benefit of not forcing the client API to explicitly deal with them. They propagate to where you want to catch them, or they go all the way out and get reported)。java的API中使用了很多非检查性异常,比如NullPointException、IllegalArgumentException、IllegalStateException等。本人更倾向于使用java自带的异常,而不是自定义异常。这些异常可以使我的代码更易理解,还可避免因为创建和捕获自定义异常增加对内存的占用。
2. 捍卫封装性
不要将特定的异常抛到上层。例如,不要将SQLException从数据访问层抛到业务对象层,业务对象层不需要知道SQLException的细节。应对这种情况,你可以有两种选择:
  • 如果在发生异常时,想通过编码进行某些处理,那么就把SQLException转换成另一种检查性异常抛出。
  • 如果不对异常进行处理,那么就转换成非检查性异常抛出。
大多数情况下,面对SQLException我们无能为力,那么直接转化成非检查性异常抛出。看看下面一段代码:
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发生时,在catch块中抛出一个RuntimeException,然后当前线程被挂起,异常信息被记录下来。这样,我没有在业务对象层添加不必要的异常处理,因为对SQLException什么也做不了。
如果你确信当发生SQLException时,业务对象层可以进行有用的处理,那么就可以将SQLException转化成有意义的检查性异常。但是多数情况下,抛出RuntimeException是比较明智的选择。(哇,很有激情啊,两点多了。嘿嘿)
注:今天继续翻译完。
3、如果没有特殊需求,不要使用自定义异常
下面这段代码有什么问题吗?
public class DuplicateUsernameException
    extends Exception {}
这个自定义异常除了一个颇具含义的名称外,对调用者没提供任何有用的信息。不要忘记java中的异常也和其他类一样,可以在其内部为调用者提供获取有价值信息的方法。
可以在DuplicateUsernameException中添加如下方法:
public class DuplicateUsernameException
    extends Exception {
    public DuplicateUsernameException 
        (String username){....}
    public String requestedUsername(){...}
    public String[] availableNames(){...}
}
加强版本中提供了两个方法:一个是requestedUsername()方法,用来返回调用方法的名称;另一个是availableNames()返回一个与调用方法名称相似的数组。这样编码时就可以指出调用方法不可用,以及哪些方法是可用的。如果没有额外的信息记录在异常中,那么直接像这样抛出标准异常即可:
throw new Exception("Username already taken");
甚至,处理异常时,除了将方法名记录下来外不会做其他处理,那么像下面一样抛出非检查型异常就好。
throw new RuntimeException("Username already taken");
当然,也可以提供方法用于检查用户名是否已被占用。
仍然要强调一下,检查性异常使用的场景是:处理异常时,通过异常提供的信息可以采取进一步处理。运行时错误,全部当做非检查性异常,这样做会使我们的代码更具可读性。
4、验证异常
你可以使用javadoc的@throws标注检查性异常和非检查型异常。但是我比较倾向于使用单元测试验证异常。测试环境下可以追踪异常,因此服务器被当做可运行文档来使用。不管使用什么方式,需要让调用者感知异常的发生。下面是一个关于 IndexOutOfBoundsException异常的例子:
public void testIndexOutOfBoundsException() {
    ArrayList blankList = new ArrayList();
    try {
        blankList.get(10);
        fail("Should raise an IndexOutOfBoundsException");
    } catch (IndexOutOfBoundsException success) {}
}
这段代码中,当blankList.get(10)被调用时,就会抛出IndexOutOfBoundsException异常。如果没引发异常,fail("Should raise an IndexOutOfBoundsException")语句会使单元测试失败。通过为异常编写单元测试,不仅可以验证异常是如何执行的,还可以通过测试特定异常,让代码变的更健壮。
Best Practices for Using Exceptions
这一部分,主要围绕“如何处理异常”。
1、自己释放资源
当数据库连接或网络连接资源使用完毕后,记得手动将它们释放。即使代码中只使用了非检查性异常,也要使用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);
        }
    }
}
DBUtil是数据库连接的工具类,这里的关键点是finally块,不论执行过程中是否发生异常,finally中的代码一定会被执行。示例中,在finally块中关闭数据库连接,如未正常关闭,则抛出RuntimeException。
2、流程控制中切勿使用异常
产生栈跟踪信息的代价很大,而且只有在调试的时候这些信息才有用。由于在流程控制中发生异常时,调用者只想知道如何处理,所以栈跟踪信息完全可以被忽略。
下面是一个在流程控制中使用自定义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()中是一个抛出异常才会终止的无限循环,这样做不仅让代码的可读性变的很差,而且严重减低了程序的运行速度。切忌只在适合的场景下使用异常。
3、不要忽略异常
当有异常抛出,这就是告诉我们需要做出处理的信号。如果捕获到的检查性异常对你毫无用处,那么直接转化成非检查性异常并再次抛出为妙,而不是使用“{}”将异常忽略,程序就好像什么都没发生一样照常执行。
4、不要捕获顶级异常
检查性异常继承自RuntimeException, RuntimeException还继承自Exception。和下面的代码一样,捕获顶级异常Exception,也同样了捕获RuntimeException异常。
try{
..
}catch(Exception ex){
}
5、异常只记录一次
多次在栈中记录同一个异常信息,会增加获取原始异常的难度,所以确保同一个异常信息只记录一次。
总结
关于异常处理的最佳方式有很多种。我没有激起检查性异常与非检查型异常之间的争论的意思,编码过程中需要根据实际的需求去设计和使用它们。我坚信随着时间的推移,还会出现更好的异常处理方式。
虽然这篇文章是2003年写的,但是其价值在今天依然是值得肯定的。(译者注)
相关资源:

三件Java开发者应该知道的事儿


这是一篇有趣的文章,应该符合那些喜欢JavaOne2012大会的人的口味。近期对Java领域专家Heinz Kabutz的一篇采访深深吸引了我,其中他的Java内存谜题程序从Java内存管理角度来看非常具有指导意义。
采访中有一段我印象特别深:Java开发者应该掌握的却至今没有掌握的知识。采访的过程中Heinz提出了很多非常好的观点,今天这篇文章就重新回顾和延展一下那天值得Java开发者关注的知识。
同时Heinz也提出了他对于未来Java8发布版本中去除HotSpot VM PermGen的担忧。

Java并发原则:是否应该注意呢?

Heinz指出,关于这这个主题的讨论通常被大部分开发者回避。除非在只编写单线程程序,否则线程并发以及相关问题是我们必须关注的问题。特别作为Java EE开发者编写的代码都会运行在高并发的环境下,一个小小的编码错误就会造成严重的线程竞争,稳定性和性能方面的问题。同样,缺乏线程相关知识,你也没法对Java EE容器的线程池做出适合的调整。
我还是建议,每个Java开发者都应该学习基本的Java并发原则知识,不论是从编码还是处理类似JVM 线程Dump分析这样的问题来讲都是非常必要的。

提升IDE使用技巧:掌握快捷键

Heinz令一个建议是每个程序员都应该深入了解Java IDE使用技巧。这个建议听起来很正常,但是你可能会惊讶只有很少一部分人可以很了解IDE的使用以及可以通过IDE提高生产效率。这种情况通常是由于缺乏对IDE的快捷键以及能力的探索。
如果你使用Eclipse,那么DZone上这篇讲述Eclipse有用快捷键的文章算是一个不错的入门。

Java内存管理:学会读懂GC logs

最后一个但不止于此:学会读懂GC logs,这条建议是我最喜欢的一个。就像之前我写过的文章中那样,JVM GC logs包含很多有关内存占用以及垃圾回收状况的重要信息。这些数据在JVM调优,以及排除Java堆空间OutOfMemoryError异常相关问题尤为重要。
不过说实话,就连想要拥有Kirk Pepperdine这样的专家一半的知识,需要花费非常多的时间,但是开始分析和了解应用中GC logs和Java内存管理基础来说是个非常好的开始。
译者注:翻译文章有翻译文章的乐趣,过程中会加深对文章内容的理解,而且一篇文章会像宝库一样,会挖掘出各种财宝。
相关阅读:(有些文章可能需要翻墙,自备翻墙工具)

Apache Shiro JSP/GSP标签库


译者:刘晓日
  • <shiro:guest/>:当前用户为游客身份,没有登录,或者没有与之关联的“Remember Me”身份标识时,并且系统没有这些限制时展现的内容,使用<shiro:guest/>标签。它与<shrio:user/>标签逻辑上是相反的。
  • <shiro:user/>:它包围起来的内容只对系统可识别的Subject,之前登录过或者在“Remember Me”服务中有记录的用户可见。值得注意的是它与<shiro:authenticated/>标签不同,后者更严格。<shiro:user/>逻辑上与<shiro:gust/>标签相反。
  • <shiro:principal/>:显示用户规则或用户的主要规则。
  • <shiro:hasPermission/>:只有当前Subject(用户)拥有某特定权限时(也就是,用户拥有某项特定能力),才显示被它包围的内容。
  • <shiro:lacksPermission/>:当前用户不具有某特定权限时,显示被它包围的内容。逻辑与<shiro:hasPermission/>相反。
  • <shiro:hasRole/>:当前用户拥有某特定角色时,显示被它包围的内容。
  • <shiro:lacksRole/>:当前用户不具有某特定角色时,显示被它包围的内容。逻辑与<shiro:hasRole/>相反。
  • <shiro:hasAnyRoles/>:当前用户拥有特定“角色集合”中的任意一个角色时,显示被它包围的内容。这个特定的“角色集合”是由角色名称组成的,使用逗号分隔角色名称。
  • <shiro:authenticated/>:用户在当前session中已得到认证时,显示被它包围的内容。它比<shiro:user>更严格,与<shiro:notAuthenticated/>标签逻辑相反。
  • <shiro:notAuthenticated/>:用户在当前session中认证失败时,显示被它包围的内容。

Apache Shiro Java注解列表


译者:刘晓日
下面是应用程序中可能会用到的Shiro支持的注解:
  • RequiresAuthentication:使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。
  • RequiresGuest:使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。
  • RequiresPermissions:当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。
  • RequiresRoles:当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。
  • RequiresUser:当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

10分钟教会你Apache Shiro


最近在研究Apache Shiro准备将自认为比较重要的资料翻译成中文(明天去拔牙,呜呜)。时间允许的情况下会尽可能多的翻译
前言
欢迎来到Apache Shiro 10分钟之旅!
希望通过这个简单、快速的示例,可以让你对应用程序中使用Shiro有个深入的了解。嗯,10分钟你应该可以搞定它。
概述
Apache Shiro是什么?
Apache Shiro一个功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。
实际上,Shiro的主要功能是管理应用程序中与安全相关的全部,同时尽可能支持多种实现方法。Shiro是建立在完善的接口驱动设计和面向对象原则之上的,支持各种自定义行为。Shiro提供的默认实现,使其能完成与其他安全框架同样的功能,这不也是我们一直努力想要得到的吗!
那么Apache Shiro能用来做什么呢?
很多,很多,嘿嘿。但是不在快速指南中做介绍,如果你想知道,那怎么办呢?去这里找寻你的答案吧。当然如果你还想知道我们什么时候,以及为什么要“创造”Shiro,去看看Shrio的历史和使命吧。
OK,现在让我们动手做点儿什么吧。
注:Shiro可以在任何环境下运行,小到最简单的命令行应用,大到大型的企业应用以及集群应用。但是我们准备在快速指南中使用最最简单的main方法的方式,让你对Shiro的API有个感官的认识。
下载
  1. 确保已经安装了JDK1.5+和Maven2.2+
  2. 这里下载最新已发布的源码。例子中我们使用1.1.0发布版本。
  3. 解压源代码
  4. 进入快速指南文件夹
    cd shiro-root-1.1.0/samples/quickstart
  5. 运行快速指南
    mvn compile exec:java
过程中会输出日志信息,用来告诉你正在进行的是什么,最后退出执行。可以在这里“samples/quickstart/src/main/java/Quickstart.java ”找到源码,也可以进行修改,记得修改后运行“mvn compile exec:java ”即可。
Quickstart.java
Quickstart.java中包含刚刚我们提到的所有内容(认证、授权等等),通过这个简单的示例可以让你轻松的熟悉Shiro的API。那么,让我们把Quickstart.java中的代码,一点一点剖析,这样便于理解它们的作用。 几乎所有的环境下,都可以通过这种方式获取当前用户:
Subject currentUser = SecurityUtils.getSubject();
通过SecurityUtils.getSubject(),就可以获取当前Subject。Subject是应用中用户的一个特定安全的缩影,虽然感觉上直接使用User会更贴切,但是实际上它的意义远远超过了User。而且每个应用程序都会有自己的用户以及框架,我们可不想和它们混淆在一起,况且Subject就是安全领域公认的名词。OK,我们继续。
在单应用系统中,调用getSubject()会返回一个Subject,它是位于应用程序中特定位置的用户信息;在服务器中运行的情况下(比如web应用),getSubject会返回一个位于当前线程或请求中的用户信息。 现在你已经得到了Subject对象,那么用它可以做什么呢?
如果你想得到应用中用户当前Session的其他参数,可以这样获取Session对象:
Session session = currentUser.getSession();
session.setAttribute( "someKey", "aValue" );
这个Session对象是Shiro中特有的对象,它和我们经常使用的HttpSession非常相似,但还提供了额外的东西,其中与HttpSession最大的不同就是Shiro中的Session不依赖HTTP环境(换句话说,可以在非HTTP 容器下运行)。
如果将Shiro部署在web应用程序中,那么这个Session就是基于HttpSession的。但是像QuickStart示例那样,在非web环境下使用,Shiro则默认使用EnterpriseSessionManagment。也就是说,不论在应用中的任何一层使用同样的API,却不需要考虑部署环境,这一优点为应用打开一个全新的世界,因为应用中要获取Session对象再也不用依赖于HttpSession或者EJB的会话Bean。而且任何客户端技术都可以共享session 数据。
现在你可以得到当前Subject和它的Session对象。那么我们如何验证比如角色和权限这些东西呢?
很简单,可以通过已得到的user对象进行验证。Subject对象代表当前用户,但是,谁才是当前用户呢?他们可是匿名用户啊。也就是说,必须登录才能获取到当前用户。没问题,这样就可以搞定:
if ( !currentUser.isAuthenticated() ) {
    //collect user principals and credentials in a gui specific manner 
    //such as username/password html form, X509 certificate, OpenID, etc.
    //We'll use the username/password example here since it is the most common.
    //(do you know what movie this is from? ;)
    UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
    //this is all you have to do to support 'remember me' (no config - built in!):
    token.setRememberMe(true);
    currentUser.login(token);
}
就是这样,太简单了吧!
那登录失败了怎么处理呢?可以通过捕获各类异常,根据不同类型的异常做出不同的处理:
try {
    currentUser.login( token );
    //if no exception, that's it, we're done!
} catch ( UnknownAccountException uae ) {
    //username wasn't in the system, show them an error message?
} catch ( IncorrectCredentialsException ice ) {
    //password didn't match, try again?
} catch ( LockedAccountException lae ) {
    //account for that username is locked - can't login.  Show them a message?
} 
    ... more types exceptions to check if you want ...
} catch ( AuthenticationException ae ) {
    //unexpected condition - error?
}
可以捕获Shiro提供的各种异常,也可以抛出自定义类异常用于处理Shiro未考虑到的情况。预知详情,可以去了解AuthenticationException JavaDoc
提示:最安全的做法是将登录失败的消息告知用户,你总不会帮助攻击者入侵你的系统吧!
OK,现在已经拥有一个登录用户了,我们还能做点儿什么呢?
比方说,他们是谁:
//print their identifying principal (in this case, a username):
log.info( "User [" + currentUser.getPrincipal() + "] logged in successfully." );
也可以判断用户是否拥有特定的角色:
if ( currentUser.hasRole( "schwartz" ) ) {
    log.info("May the Schwartz be with you!" );
} else {
    log.info( "Hello, mere mortal." );
}
还可以判断用户是否对特定某实体有操作权限:
if ( currentUser.isPermitted( "lightsaber:weild" ) ) {
    log.info("You may use a lightsaber ring.  Use it wisely.");
} else {
    log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
当然,还可以进行功能强大的实例级别的权限验证。通过它可以判断用户是否有访问特定类型实例的权限:
if ( currentUser.isPermitted( "winnebago:drive:eagle5" ) ) {
    log.info("You are permitted to 'drive' the 'winnebago' with license plate (id) 'eagle5'.  " +"Here are the keys - have fun!");
} else {
    log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
小菜一碟,对吧。
最后,当用户使用完毕,还可以退出应用。
currentUser.logout(); //removes all identifying information and invalidates their session too.
这些就是使用Apache Shiro开发应用的核心了,当然,Apache Shiro已将将很多复杂的东西封装在内部了,但是现在它就是这么简单。
你会有疑问吧,用户登录时,谁负责把用户信息(用户名、密码、角色、权限等)取出来,还有运行时,谁负责安全认证呢?当然由你决定了啊。通过将一个实现了Shiro中的Realm的Reaml配置到Shiro中即可。
至于如何配置很大程度上取决于你的运行时环境,比如在单应用、web应用、基于Spring或JEE 容器的应用或者组合模式中使用Shiro,配置都有所不同。如何配置已经超出QuickStart示例的范围,因为它的主要目的是帮助你熟悉Shiro的API和概念。
如果想进一步了解Shiro,可以看看Authentication GuideAuthorization Guide。也可以查看其他文档(特别是Reference Manual),这里可以解决你的各种疑问。
感谢一路同行,希望你能喜欢使用Apache Shiro。