一、Java 异常体系结构
从上面异常继承树可以看出,所以异常都继承自Throwable
,这也意味着所有异常都是可以抛出的。具体来说,广义的异常可以分为Error
和Exception
两大类。
Error表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如:最常见的OOM(OutOfMemoryError)错误。对于Error我们编程中基本是用不到的,也就是说我们在编程中可以忽略Error错误。所以我们通常所说的异常只的是Exception,而Exception可分为检查异常和非检查异常。
二、检查异常与非检查异常
通常我们所说的异常指的都是Exception的子类,它们具体可以分为两大类在Java,Exception
的子类和RuntimeException
的子类,它们分别对应着检查异常和非检查异常。
Checked exception
检查异常,继承自Exception
类。对于检查异常,Java强制我们必须进行处理。对于抛出检查异常的API我们有两种处理方式:
- 对抛出检查异常的API进程try catch
- 继续把检查异常往上抛
常见的检查异常有:
SQLException 、IOException 、InterruptedException
Unchecked exception
非检查异常,也称运行时异常RuntimeException
,继承自RuntimeException
,所有非检查都有个特点,就是代码不需要处理它们的异常也能通过编译,所以它们称作unchecked exception。RuntimeException
本身也是继承自Exception
。
常见的非检查异常有:
NullPointerException、IllegalArgumentException、NumberFormatException、IndexOutOfBoundsException
ps:其实Error也是一个非检查异常。
三、检查异常 vs 非检查异常
1、非检查异常由于没有提示调用者捕获异常,一但异常触发,当前线程就会停止。
2、检查异常明确提示调用者捕获异常,通过对异常的捕获处理,可以使线程从异常中恢复。
3、有时候异常触发后,还要有一些后续工作要完成。如:向文件中写数据时出现异常,必须使用检查异常,提示调用者捕获,在final中关闭流,因此IOException是检查异常; SQLException也是检查性异常,因为这种异常出现,可能要做回滚操作。
4、检查异常会强制调用者对其进行try catch或者往上层抛,这样就给调用者造成了不必要的负担。
5、检查异常和非检查异常在选择时,如果没有特殊要求,一般使用非检查异常。
四、异常的架构设计
1、异常隔离
我们经常将代码分Controller、Service、DAO 等不同的层次结构,DAO 层中会包含抛出异常的方法。
如:
public class Test {
public static void main(String[] args) {
controller();
}
public static void controller() {
try {
services();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void services() throws SQLException {
dao();
}
public static void dao() throws SQLException {
throw new SQLException("数据库连接失败");
}
}
上面这段代码咋一看没什么问题,但是从设计耦合角度仔细考虑一下,Controller 层需要显式的 try-catch 捕捉或者抛出 dao层的异常。这里的 SQLException 显然污染到了上层调用代码,根据设计隔离原则,我们可以适当修改成:
public class Test {
public static void main(String[] args) {
controller();
}
public static void controller() {
services();
}
public static void services() {
try {
dao();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public static void dao() throws SQLException {
throw new SQLException("数据库连接失败");
}
}
异常转译就是将一种异常转换为另一种异常。需要注意的是,我们在对异常进行转译的时候一定要在构造方法中传入原异常的throwable对象,这样可以保留原异常栈信息,而不是把原异常用另一个异常完全替换掉。
当然,我们也可以根据实际情况将一个非检查异常包装成一个检查异常。
2、设计一个统一的异常处理类
一般当程序发生异常时,通常异常处理可能需要做一些通用处理,如异常日志记录、异常通知,重定向到一个统一的错误页面(如 Web 应用)等。如果这些通用异常处理放置于 catch 块中,将导致大量的重复代码,从而可能引起日志冗余、同一异常的实现多样化等问题。另外,大量异常处理程序放置于 catch 块中造成程序的高耦合性。为了解决此类问题,有必要分离出异常处理程序、统一异常处理风格、降低耦合性、增强异常处理模块的复用程度。通常的异常处理模式包括业务委托模式(Business Delegate)、前端控制器模式(Front Controller)、拦截过滤器模式(Intercepting Filter)、AOP 模式、模板方法模式等。
在web编程中,一般对控制层的异常都应该做统一处理,因为控制层向上面对用户,所以我们要在控制层捕获service所有可能出现的异常。上面也提到我们在控制层对每个service调用进行try catch显然会很繁琐而且也会导致大量重复代码,所以在遇到这种情况时我们一定要考虑引入统一异常处理机制,而很多框架也提供了这样的处理机制,如Spring的AOP,SpringMVC的 ExceptionHandler、RESTEasy的ExceptionMapper。
对于Spring MVC框架统一异常处理机制请参考:Spring MVC 中的异常处理 (handling exceptions)
对于Restful框架的统一异常处理机制请参考: RESTEasy中的通用异常处理ExceptionMapper
3、异常层次定义
异常层次结构应该以一种普遍通用的原则定义。为此,我们可以利用面向对象语言具备多态的性质,隐藏异常的实际实现。对于异常 service 而言,只需要捕获最基本的应用程序异常 AppException,异常处理过滤器会自动过滤实际异常类型并找到相应的异常处理器。另外,在方法的 throws 语句中勿需放入大量的检查异常;对方法调用者也不会出现混乱的 catch 块,最多可能只存在一个用于处理基本应用程序异常 AppException(委托给异常 service 处理)。
前面的章节过,应用系统异常可以从用户和开发者两个视角去考虑。因此,我们可以把异常划分为业务操作异常和系统内部运行时异常两种类型。抛出业务级异常或系统运行时异常的决策,需要与应用系统本身的架构层次相结合,考虑所要处理异常的层次。如图所示为一个典型的异常层次结构:
其中,BussinessException 属于基本业务操作异常,所有业务操作异常都继承于该类。例如,通常 UI 层或 Web 层是由系统最终用户执行业务操作驱动,因此最好抛出业务类异常。ServiceException 一般属于中间服务层异常,该层操作引起的异常一般包装成基本 ServiceException。DAOException 属于数据访问相关的基本异常。
对于多层系统,每一层都有该层的基本异常。在层与层之间的信息传递与方法调用时候,一旦在某层发生异常,传递到上一层的时候,一般包装成该层异常,直至与用户最接近的 UI 层,从而转化成用户友好的错误信息。