10.4.5 异常链

10.4.5 异常链

对于真实的企业级应用而言,常常有严格的分层关系,层与层之间有非常清晰的划分,上层功能的实现严格依赖于下层的API,也不会跨层访问。图10.5显示了这种具有分层结构应用的大致示意图。
这里有一张图片
对于一个采用图10.5所示结构的应用,当业务逻辑层访问持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面,有如下两个原因。

  • 对于正常用户而言,他们不想看到底层SQLException异常,SQLException异常对他们使用该系统没有任何帮助。
  • 对于恶意用户而言,将SQLException异常暴露出来不安全。

    异常转译

    把底层的原始异常直接传给用户是一种不负责任的表现。通常的做法是:

程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。假设程序需要实现工资计算的方法,则程序应该采用如下结构的代码来实现该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public calSal() throws SalException
{
try
{
//实现结算工资的业务逻辑
...
}
catch(SQLException sqle)
{
//把原始异常记录下来,留给管理员
...
//下面异常中的 message就是对用户的提示
throw new SalException("访问底层数据库出现异常");
}
catch(Exception e)
{
//把原始异常记录下来,留给管理员
...
//下面异常中的message就是对用户的提示
throw new SalException("系统出现未知异常");
}
}

这种把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节,这完全符合面向对象的封装原则。

职责链模式

这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来的做法是一种典型的链式处理(23种设计模式之一:职责链模式),也被称为”异常链“。

JDK1.4以前,程序员必须自己编写代码来保持原始异常信息。从JDK1.4以后,所有Throwable的子类在构造器中都可以接收一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,你也能通过这个异常链追踪到异常最初发生的位置。例如希望通过上面的SalException去追踪到最原始的异常信息,则可以将该方法改写为如下形式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public calSal() throws SalException
{
try
{
//实现结算工资的业务逻辑
...
}
catch(SQLException sqle)
{
//把原始异常记录下来,留给管理员
...
//下面异常中的sqle就是原始异常
throw new SalException(sqle);//代码1
}
catch(Exception e)
{
//把原始异常记录下来,留给管理员
...
//下面异常中的e就是原始异常
throw new SalException(e);//代码2
}
}

上面程序中的代码1,代码2在创建SalException对象时,传入了一个Exception对象,而不是传入了一个String对象,这就需要SalException类有相应的构造器。从JDK1.4以后, Throwable基类已有了一个可以接收Exception参数的方法,所以可以采用如下代码来定义SalException类。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SalException extends Exception
{
public SalException(){}
public SalException(String msg)
{
super(msg);
}
// 创建一个可以接受Throwable参数的构造器
public SalException(Throwable t)
{
super(t);
}
}

创建了这个SalException业务异常类后,就可以用它来封装原始异常,从而实现对异常的链式处理

总结

要支持异常链,则自定义异常类需要创建三个构造器:

  • 一个无参构造器,
  • 一个带String参数的构造器,用来传递异常信息
  • 一个带Throwable参数的构造器,用来传入原始的异常