小心例外带来的风险(2)

发表于:2007-05-25来源:作者:点击数: 标签:风险例外小心带来上文
接上文: 小心例外带来的风险(1) 不要捕获泛型例外 在复杂的软件中,经常会有一些特定的代码块执行时会抛出多种不同例外的方法。动态装入一个类和实例化一个对象都可能会产生几个不同的例外,包括ClassNotFoundException,Inst ant iationException,IllegalAc
接上文:小心例外带来的风险(1)

不要捕获泛型例外

在复杂的软件中,经常会有一些特定的代码块执行时会抛出多种不同例外的方法。动态装入一个类和实例化一个对象都可能会产生几个不同的例外,包括ClassNotFoundException, InstantiationException, IllegalAclearcase/" target="_blank" >ccessException, 和 ClassCastException。
一个繁忙的程序员在遇到这种情况时可能简单的把方法调用包在一个只会捕获泛型例外Exception的try/catch块,而不是添加四个不同的catch块到try块后面(看下面的代码清单3)。这看起来似乎无可置否,却会产生一些无意识的副面效果。例如,如果className()是null,那么Class.forName()将会抛出一个NullPointerException例外并在这个方法中被捕获。在这种情况下,catch块将捕获此例外虽然它从没打算去捕获这样一个例外,只是因为NullPointerException是RuntimeException的一个子类,而且RuntimeException又是Exception的一个子类。所以一个普通的catch(Exception e)将会捕获所有RuntimeException的子类,包括NullPointerException, IndexOutOfBoundsException, 和ArrayStoreException。通常,一个程序员并不打算去捕获这些例外。
在代码清单3中,null的className会导致一个NullPointerException例外产生,它告诉在调用的方法中类名无效。
代码清单3
  1. public SomeInterface buildInstance(String className) {
  2.    SomeInterface impl = null;
  3.    try {
  4.       Class clazz = Class.forName(className);
  5.       impl = (SomeInterface)clazz.newInstance();
  6.    }
  7.    catch (Exception e) {
  8.       log.error("Error creating class: " + className);
  9.    }
  10.    return impl;
  11. }

另一个使用泛型捕获子句的结果是限制日志记录,因为catch不知道到底那一个特殊的例外被捕获。有些程序员在面对这种问题的时候,采取添加检测的手段去查看例外的类型(代码清单4),而这正好与使用catch块的目的相背离。
代码清单4
  1. catch (Exception e) {
  2.       if (e instanceof ClassNotFoundException) {
  3.          log.error("Invalid class name: " + className + ", " + e.toString());
  4.       }
  5.       else {
  6.          log.error("Cannot create class: " + className + ", " + e.toString());
  7.       }
  8.    }

代码清单5提供一种完整的捕获特殊例外的例子,一些程序员可能会对它感趣。操作符instanceof不是必须的因为这个特殊的例外自会被捕获。每一个被检查的例外(ClassNotFoundException, InstantiationException, IllegalAccessException) 会被捕获和处理。对于一个类装入正确,但是却没有实现SomeInterface接口这种特殊情况会产生一个ClassCastException例外,这个例外也会被查证。
代码清单5

  1. public SomeInterface buildInstance(String className) {
  2.    SomeInterface impl = null;
  3.    try {
  4.       Class clazz = Class.forName(className);
  5.       impl = (SomeInterface)clazz.newInstance();
  6.    }
  7.    catch (ClassNotFoundException e) {
  8.       log.error("Invalid class name: " + className + ", " + e.toString());
  9.    }
  10.    catch (InstantiationException e) {
  11.       log.error("Cannot create class: " + className + ", " + e.toString());
  12.    }
  13.    catch (IllegalAccessException e) {
  14.       log.error("Cannot create class: " + className + ", " + e.toString());
  15.    }
  16.    catch (ClassCastException e) {
  17.       log.error("Invalid class type, " + className
  18.          + " does not implement " + SomeInterface.class.getName());
  19.    }
  20.    return impl;
  21. }

在某些情况下,更好的方法是重新抛出一个已知的例外(或者叫创建一个新的例外)而不是试图去在当前这个方法中处理。这允许调用方法通过放置这个例外到一个已知的上下文中去处理这种错误情形。

下面的代码清单6提供了一个buildInterface()方法的替换版本。如果在装入和实例化类时发生问题,这个版本会抛出一个ClassNotFoundException例外。在这个例子中,调用方法会确保得到一个正确的实例化对象或者是一个例外。这样调用方法就不需要去检查返回的对象是否为空了。
注意这个例子使用了Java 1.4的方法来创建一个已经被另外的例外封装的新的例外,以便保存原始的堆栈跟踪信息。否则,堆栈跟踪将指明方法buildInstance()是引起例外的源,而不是潜在的由newInstance()抛出的例外。
代码清单6
  1. public SomeInterface buildInstance(String className)
  2.      throws ClassNotFoundException {
  3.    try {
  4.       Class clazz = Class.forName(className);
  5.       return (SomeInterface)clazz.newInstance();
  6.    }
  7.    catch (ClassNotFoundException e) {
  8.       log.error("Invalid class name: " + className + ", " + e.toString());
  9.       throw e;
  10.    }
  11.    catch (InstantiationException e) {
  12.       throw new ClassNotFoundException("Cannot create class: " + className, e);
  13.    }
  14.    catch (IllegalAccessException e) {
  15.       throw new ClassNotFoundException("Cannot create class: " + className, e);
  16.    }
  17.    catch (ClassCastException e) {
  18.       throw new ClassNotFoundException(className
  19.         + " does not implement " + SomeInterface.class.getName(), e);
  20.    }
  21. }

在有些情况下,这段代码可能无法从某种错误状态恢复,这时,捕获一个特殊的例外以使代码能指出某种状态是否是可恢复的就变得很重要了。请试着以这种观点去看代码清单6中类实例化的例子。

在代码清单7中,如果className无效,程序会返回一个缺省的对象,并且抛出一个例外以指明非法的操作,比如错误的转型或访问权限不够。
注意:IllegalClassException是一系列的例外类,为示范的目的而在这提及(译注:并不是Java标准库所带)。
代码清单7
  1. public SomeInterface buildInstance(String className)
  2.      throws IllegalClassException {
  3.    SomeInterface impl = null;
  4.    try {
  5.       Class clazz = Class.forName(className);
  6.       return (SomeInterface)clazz.newInstance();
  7.    }
  8.    catch (ClassNotFoundException e) {
  9.       log.warn("Invalid class name: " + className + ", using default");
  10.    }
  11.    catch (InstantiationException e) {
  12.       log.warn("Invalid class name: " + className + ", using default");
  13.    }
  14.    catch (IllegalAccessException e) {
  15.       throw new IllegalClassException("Cannot create class: " + className, e);
  16.    }
  17.    catch (ClassCastException e) {
  18.       throw new IllegalClassException(className
  19.         + " does not implement " + SomeInterface.class.getName(), e);
  20.    }
  21.    if (impl == null) {
  22.       impl = new DefaultImplemantation();
  23.    }
  24.    return impl;
  25. }


什么时候应捕获一个泛型例外?


在某些情况下捕获一个泛型例外是可以的,比如当它很便利且必需去捕获一个泛型例外的时候。这种情况非常特殊,且对于大型、允许失败的系统来说很重要。在代码清单8中,从一个请求队列中读取请求并顺序处理。但是,当请求被处理时候如果有任何例外发生(一个BadRequestException或任何RuntimeException的子类,包括NullpointerException),则那个例外就会被while循环外部所捕获。这样任何错误都会引起循环终止并且任何剩余的请求都不会被处理。那意味着在请求处理期间去处理一个错误是一种较差的方法。
代码清单8
  1. public void processAllRequests() {
  2.    Request req = null;
  3.    try {
  4.       while (true) {
  5.         req = getNextRequest();
  6.          if (req != null) {
  7.             processRequest(req); // throws BadRequestException
  8.          }
  9.          else {
  10.             // Request queue is empty, must be done
  11.               break;
  12.          }
  13.       }
  14.    }
  15.    catch (BadRequestException e) {
  16.       log.error("Invalid request: " + req, e);
  17.    }
  18. }

操作请求处理的一种较好的方法是对上述代码的逻辑作两个重要的改变,首先,将try/catch块移入请求处理的循环中。那样的话任何错误都会在循环内部被捕获和处理,并且不会引起循环终止。因而,循环会继续处理请求,即使一个单个的请求失败。第二,更改这个try/catch块使它去捕获一个泛型的例外。这样,任何例外都在循环内部被捕获,请求也得以继续处理。请看下面代码清单9:
代码清单9:
  1. public void processAllRequests() {
  2.    while (true) {
  3.       Request req = null;
  4.       try {
  5.          req = getNextRequest();
  6.          if (req != null) {
  7.             processRequest(req); // Throws BadRequestException
  8.       }
  9.          else {
  10.             // Request queue is empty, must be done
  11.               break;
  12.          }
  13.       }
  14.       catch (Exception e) {
  15.          log.error("Error processing request: " + req, e);
  16.       }
  17.    }
  18. }


捕获一个泛型例外听起来好像直接与本篇开始的观点相违背,确实如此。但是,这只是在非常非常特殊的的环境下。在这种情况下,捕获一个泛型的例外以防止一个单个的例外终止整个系统运行。
在请求、事务或事件在一个循环中被处理的情况下,即使在处理期间有例外被抛出,那个循环仍需要执行以便继续去处理。
在代码清单9中,在处理循环中try/catch块被认为是顶级例外管理者(the top-level exception handler),并且它需要捕获和记录所有在这个代码级别引起的例外。这样,例外没有被忽略,也不会丢失,并且例外不会中断余下需要处理的请求。

每个大型、复杂的系统都有一个顶级例外管理者(或者是每一个子系统,这取决于系统如何完成处理的)。顶级例外管理者无意去修复由例外引起的潜在问题,并且能在不终止处理的情况下捕获和记录这个问题。这并不是暗示所有的例外都应该在这个级别被抛出。任何例外如果能在较低级别被处理,则就应该那么做。如果那样,例外处理对问题发生时的状态逻辑就会知道的更多。但是,如果例外不能在较低级别被处理,那么抛出它到更高级别,这样,所有那些不可恢复的错误都将在一处被处理(顶级例外管理者),而不是遍及整个系统。

不要抛掷泛型例外


出现在程序清单1中的整个问题是在程序员决定从cleanupEverything()方法中抛掷泛型例外开始的,如此一来代码变得很优美,而当一个方法抛出6个不同例外时则会变得杂乱:方法声明变得得以理解,调用方法也不得不捕获那6个不同的例外,就像代码清单10那样。
代码清单10
  1. public void cleanupEverything() throws
  2.       ExceptionOne, ExceptionTwo, ExceptionThree,
  3.       ExceptionFour, ExceptionFive, ExceptionSix {
  4.    cleanupConnections();
  5.    cleanupFiles();
  6.    removeListeners();
  7. }
  8. public void done() {
  9.    try {
  10.       doStuff();
  11.       cleanupEverything();
  12.       doMoreStuff();
  13.    }
  14.    catch (ExceptionOne e1) {
  15.       // Log e1
  16.    }
  17.    catch (ExceptionTwo e2) {
  18.       // Log e2
  19.    }
  20.    catch (ExceptionThree e3) {
  21.       // Log e3
  22.    }
  23.    catch (ExceptionFour e4) {
  24.       // Log e4
  25.    }
  26.    catch (ExceptionFive e5) {
  27.       // Log e5
  28.    }
  29.    catch (ExceptionSix e6) {
  30.       // Log e6
  31.    }
  32. }

但是,即使代码有点杂乱,却很清楚。使用特殊例外可以避免两种非常真实的问题:抛掷一个泛型Exception隐藏了潜在问题的细节,这样也就失去了处理问题之所在的机会。更进一步说,抛掷一个泛型例外会强制任何调用这个的方法的代码要么捕获那个泛型例外(正像前述一样,这种方法有问题),要么重新抛掷那个泛型例外扩大问题范围。
有代表性的是,当一个方法声明它将抛出一个泛型例外Exception时,它这样做可能有以下两个原因之一:一种原因是,这个方法调用了几个另外的方法,而那几个方法可能会抛掷出许多不同的例外(比如调停者模式或门面模式),且隐藏了例外状态的细节。因此不论是什么问题这个方法只是简单的声明它会抛出Exception,而不创建和抛掷一层例外(封装在较低级别的例外)。另外一种情形是,在方法实例化和抛掷泛型例外Exception(即throw new Exception())的地方,因为程序员认为例外事实上不应该被用来表达这种情形。

这两方面的问题只要稍微思考和设计,就都可解决。什么是细节?那一层例外真的应该被抛掷吗?这个设计可能包括仅仅声明这个方法会抛掷一些确实会发生的例外。另一个选择是创建一层例外去封装抛掷和声明例外的东西。在大多数情况下,被方法抛掷的例外(或者叫一系列例外)应该尽可能的详细。这种更详细的例外会提供更多关于错误状态的信息,这样就允许这种情形被处理或至少详细的被记录。

如果泛型Exception类被选中,那就意味着任何调用一个声明了会抛出Exception方法的方法,要么必须声明它本身也会抛出Exception,要么封装这个方法调用在一个捕获泛型Exception的try/catch块中。我用这种方法在前面解释了这个问题。

小心使用泛型例外


这篇文章探究了处理泛型例外的几个方面:它们永远都不要被抛掷,也不应被忽略。它们应该很少被捕获(只在非常特殊的情况下)。它们不会提供详细信息以允许你去有效的处理它们,所以你不打算那样做时你应该小心捕获例外。

例外是Java中的一种强有力的工具。如果你正确使用,它能使你成为一个更有效率的程序员,并且缩短你的开发周期,特别是在测试和调试时。当例外被错误的使用时,在你的系统中,由于隐藏了问题的所在,你得一次又一次的重复工作。所以你要关注好你在哪和如何使用泛型例外。

原文转自:http://www.ltesting.net

评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)