1.4.2 创建自己的异常
和程序中的其他部分一样,异常部分也需要经过仔细的考虑和设计。开发人员一般会花费大量的精力对程序的主要功能部分进行设计,而忽略对于异常的设计。这会对程序的整体架构造成影响。在对异常部分进行设计的时候,考虑下面几个建议。
1.精心设计异常的层次结构
一般来说,一个程序中应该要有自己的异常类的层次结构。如果只打算使用非受检异常,至少需要一个继承自RuntimeException的异常类。如果还需要使用受检异常,还要有另外一个继承自Exception的异常类。如果程序中可能出现的异常情况比较多,应该在不同的抽象层次上定义相关的异常,并形成一个完整的层次结构。这个异常的层次结构与程序本身的类层次结构是相对应的。不同抽象层次上的代码应该只声明抛出同一层次上的相关异常。
比如一个典型的Web应用按照自顶向下的顺序一般分成展现层、服务层和数据访问层。与之对应的异常也应该按照这个层次结构来进行划分。数据访问层的代码应该只声明抛出与访问数据相关的异常,如数据库连接和操作相关的异常。这么做的好处是工作于某个抽象层次上的开发人员不需要去了解其他层次上的细节。比如服务层开发人员会调用数据访问层的代码,他只需要关心数据访问可能出现异常即可,而并不需要去关心这是一个数据库访问异常,还是一个文件系统访问异常。这种抽象层次的划分对系统的演化是比较重要的。假如系统以后不再使用数据库作为数据访问的实现,服务层的异常处理逻辑也不会受到影响。
一般来说,对于程序中可能出现的各种错误,都需要声明一个异常类与之对应。有些开发人员会选择一个大而全的异常类来表示各种不同类型的错误,利用这个异常的消息来区分不同的错误。比如声明一个异常类BaseException,不管是数据访问错误还是用户输入的数据格式不对,都会抛出同一个异常,只是使用的消息内容不同。当采用这种异常设计方式的时候,异常的处理者只能根据异常消息字符串的内容来判断具体的错误类型。如果异常的处理者只是简单地进行日志记录或重新抛出此异常,这种方式并没有太大的问题。如果异常的处理者需要解析异常的消息格式来判断具体类型,那么这种方式就是不可取的,应该换成不同的异常类。
采用这种异常层次结构会遇到的一个常见的异常处理模式是包装异常。包装异常的目的在于使异常只出现在其所对应的抽象层次上。当一个异常抛出的时候,如果没有被捕获到,就会一直沿着调用栈往上传递,直到被上层方法捕获或是最终由Java虚拟机来处理。这种传递方式会使这个异常跨越多个抽象层次的边界,使得上层代码看到不需要关注的底层异常。为此,在一个异常要跨越抽象层次边界的时候,需要进行包装。包装之后的异常才是上层代码需要关注的。
对一个异常进行包装是一件非常简单的事情。从JDK 1.4开始,所有异常的基类java.lang.Throwable就支持在构造方法中传入另外一个异常作为参数。而这个参数所表示的异常被包装在新的异常中,可以通过getCause方法来获取。代码清单1-6给出了一个异常包装的示例,即把底层的IOException包装成更为抽象的DataAccessException。使用DataAccessGateway类的上层代码只需要知道DataAccessException即可,并不需要知道IOException的存在。
代码清单1-6 使用异常包装技术的示例
public class DataAccessGateway{
public void load()throws DataAccessException{
try{
FileInputStream input=new FileInputStream("data.txt");
}
catch(IOException e){
throw new DataAccessException(e);
}
}
}
在使用异常包装的时候,一个典型的做法就是为每个层次定义一个基本的异常类。这个层次的所有公开方法在声明异常的时候都使用这个异常类。所有这个层次中出现的底层异常都被包装成这个异常。
2.异常类中包含足够的信息
异常存在的一个很重要的意义在于,当错误发生的时候,调用者可以对错误进行处理,从产生的错误中恢复。为了方便调用者处理这些异常,每个异常中都需要包含尽量丰富的信息。异常不应该只说明某个错误发生了,还应该给出相关的信息。异常类是完整的Java类,因此在其中添加所需的域和方法是一件很简单的事情。
考虑下面一个场景,当用户进行支付的时候,如果他的当前余额不足以完成支付,那么在所抛出的异常信息中,可以包含当前所需的金额、余额和其中的差额等信息。这样异常处理者就可以提供给用户更加具体的出错信息以及更加明确的解决方案。
3.异常与错误提示
对于与用户进行交互的程序来说,需要适当区分异常与展示给用户的错误提示。通常来说,异常指的是程序的内部错误。与异常相关的信息,主要是供开发人员调试时使用的。这些信息对于最终用户来说是没有意义的。一般来说,普通用户除了重新执行出错的操作之外,没有其他应对办法。因此,程序需要保证在直接与用户交互的代码层次上,捕获所有的异常,并生成相应的错误提示。比如在一个servlet中,要确保在产生HTTP响应的时候捕获全部的异常,以避免用户看到一个包含异常堆栈信息的错误页面。
有些开发人员会直接将异常自带的消息作为给用户的错误提示。这个时候需要注意异常消息的国际化问题。只需要把异常与Java中的java.util.ResourceBundle结合起来,就可以很容易地实现异常消息的国际化。代码清单1-7给出了一个支持国际化异常消息的异常类的基类LocalizedException。
代码清单1-7 支持国际化异常消息的异常类的基类
public abstract class LocalizedException extends Exception{
private static final String DEFAULT_BASE_NAME="com/java7book/chapter1/
exception/java7/messages";
private String baseName=DEFAULT_BASE_NAME;
protected ResourceBundle resourceBundle;
private String messageKey;
public LocalizedException(String messageKey){
this.messageKey=messageKey;
initResourceBundle();
}
public LocalizedException(String messageKey, String baseName){
this.messageKey=messageKey;
this.baseName=baseName;
initResourceBundle();
}
private void initResourceBundle(){
resourceBundle=ResourceBundle.getBundle(baseName);
}
protected void setBaseName(String baseName){
this.baseName=baseName;
}
protected void setMessageKey(String key){
messageKey=key;
}
public abstract String getLocalizedMessage();
public String getMessage(){
return getLocalizedMessage();
}
protected String format(Object……args){
String message=resourceBundle.getString(messageKey);
return MessageFormat.format(message, args);
}
}
在使用的时候,每个需要国际化的异常类只需要继承LocalizedException,并实现getLocalizedMessage方法即可。代码清单1-8是之前提到的支付余额不足时抛出的异常类。在子类的构造方法中指定异常消息在消息资源文件中对应的名称。使用format方法可以对消息进行格式化。
代码清单1-8 继承自支持国际化异常消息的异常类的子类
public class InsufficientBalanceException extends LocalizedException{
private BigDecimal requested;
private BigDecimal balance;
private BigDecimal shortage;
public InsufficientBalanceException(BigDecimal requested, BigDecimal balance){
super("INSUFFICIENT_BALANCE_EXCEPTION");
this.requested=requested;
this.balance=balance;
this.shortage=requested.subtract(balance);
}
public String getLocalizedMessage(){
return format(balance, requested, shortage);
}
}