什么时候应捕获一个泛型异常?
在某些情况下捕获一个泛型异常是可以的,比如当它很便利且必需去捕获一个泛型异常的时候。这种情况非常特殊,且对于大型、允许失败的系统来说很重要。在代码清单8中,从一个请求队列中读取请求并顺序处理。但是,当请求被处理时候如果有任何异常发生(一个BadRequestException或任何RuntimeException的子类,包括NullpointerException),则那个异常就会被while循环外部所捕获。这样任何错误都会引起循环终止并且任何剩余的请求都不会被处理。那意味着在请求处理期间去处理一个错误是一种较差的方法。
代码清单8
public void processAllRequests() {
Request req = null;
try {
while (true) {
req = getNextRequest();
if (req != null) {
processRequest(req); // throws BadRequestException
}else{
// Request queue is empty, must be done
break;
}
}
}catch (BadRequestException e) {
log.error("Invalid request: " + req, e);
}
}
操作请求处理的一种较好的方法是对上述代码的逻辑作两个重要的改变,首先,将try/catch块移入请求处理的循环中。那样的话任何错误都会在循环内部被捕获和处理,并且不会引起循环终止。因而,循环会继续处理请求,即使一个单个的请求失败。第二,更改这个try/catch块使它去捕获一个泛型的异常。这样,任何异常都在循环内部被捕获,请求也得以继续处理。请看下面代码清单9:
代码清单9:
public void processAllRequests() {
while (true) {
Request req = null;
try {
req = getNextRequest();
if (req != null) {
processRequest(req); // Throws BadRequestException
}else {
// Request queue is empty, must be done
break;
}
}catch (Exception e) {
log.error("Error processing request: " + req, e);
}
}
}
捕获一个泛型异常听起来好像直接与本篇开始的观点相违背,确实如此。但是,这只是在非常非常特殊的的环境下。在这种情况下,捕获一个泛型的异常以防止一个单个的异常终止整个系统运行。
在请求、事务或事件在一个循环中被处理的情况下,即使在处理期间有异常被抛出,那个循环仍需要执行以便继续去处理。
在代码清单9中,在处理循环中try/catch块被认为是顶级异常管理者(the top-level exception handler),并且它需要捕获和记录所有在这个代码级别引起的异常。这样,异常没有被忽略,也不会丢失,并且异常不会中断余下需要处理的请求。
每个大型、复杂的系统都有一个顶级异常管理者(或者是每一个子系统,这取决于系统如何完成处理的)。顶级异常管理者无意去修复由异常引起的潜在问题,并且能在不终止处理的情况下捕获和记录这个问题。这并不是暗示所有的异常都应该在这个级别被抛出。任何异常如果能在较低级别被处理,则就应该那么做。如果那样,异常处理对问题发生时的状态逻辑就会知道的更多。但是,如果异常不能在较低级别被处理,那么抛出它到更高级别,这样,所有那些不可恢复的错误都将在一处被处理(顶级异常管理者),而不是遍及整个系统。
不要抛掷泛型异常
出现在程序清单1中的整个问题是在程序员决定从cleanupEverything()方法中抛掷泛型异常开始的,如此一来代码变得很优美,而当一个方法抛出6个不同异常时则会变得杂乱:方法声明变得得以理解,调用方法也不得不捕获那6个不同的异常,就像代码清单10那样。
代码清单10
public void cleanupEverything() throws
ExceptionOne, ExceptionTwo, ExceptionThree,
ExceptionFour, ExceptionFive, ExceptionSix {
cleanupConnections();
cleanupFiles();
removeListeners();
}
public void done() {
try {
doStuff();
cleanupEverything();
doMoreStuff();
}catch (ExceptionOne e1) {
// Log e1
}catch (ExceptionTwo e2) {
// Log e2
}catch (ExceptionThree e3) {
// Log e3
}catch (ExceptionFour e4) {
// Log e4
}catch (ExceptionFive e5) {
// Log e5
}catch (ExceptionSix e6) {
// Log e6
}
}
但是,即使代码有点杂乱,却很清楚。使用特殊异常可以避免两种非常真实的问题:抛掷一个泛型Exception隐藏了潜在问题的细节,这样也就失去了处理问题之所在的机会。更进一步说,抛掷一个泛型异常会强制任何调用这个的方法的代码要么捕获那个泛型异常(正像前述一样,这种方法有问题),要么重新抛掷那个泛型异常扩大问题范围。
有代表性的是,当一个方法声明它将抛出一个泛型异常Exception时,它这样做可能有以下两个原因之一:一种原因是,这个方法调用了几个另外的方法,而那几个方法可能会抛掷出许多不同的异常(比如调停者模式或门面模式),且隐藏了异常状态的细节。因此不论是什么问题这个方法只是简单的声明它会抛出Exception,而不创建和抛掷一层异常(封装在较低级别的异常)。另外一种情形是,在方法实例化和抛掷泛型异常Exception(即throw new Exception())的地方,因为程序员认为异常事实上不应该被用来表达这种情形。
这两方面的问题只要稍微思考和设计,就都可解决。什么是细节?那一层异常真的应该被抛掷吗?这个设计可能包括仅仅声明这个方法会抛掷一些确实会发生的异常。另一个选择是创建一层异常去封装抛掷和声明异常的东西。在大多数情况下,被方法抛掷的异常(或者叫一系列异常)应该尽可能的详细。这种更详细的异常会提供更多关于错误状态的信息,这样就允许这种情形被处理或至少详细的被记录。
如果泛型Exception类被选中,那就意味着任何调用一个声明了会抛出Exception方法的方法,要么必须声明它本身也会抛出Exception,要么封装这个方法调用在一个捕获泛型Exception的try/catch块中。我用这种方法在前面解释了这个问题。
小心使用泛型异常
这篇文章探究了处理泛型异常的几个方面:它们永远都不要被抛掷,也不应被忽略。它们应该很少被捕获(只在非常特殊的情况下)。它们不会提供详细信息以允许你去有效的处理它们,所以你不打算那样做时你应该小心捕获异常。
异常是Java中的一种强有力的工具。如果你正确使用,它能使你成为一个更有效率的程序员,并且缩短你的开发周期,特别是在测试和调试时。当异常被错误的使用时,在你的系统中,由于隐藏了问题的所在,你得一次又一次的重复工作。所以你要关注好你在哪和如何使用泛型异常。