Java异常控制机制和异常处理原则【转】

 

原文:https://www.jianshu.com/p/15872cba211d

Java异常控制机制又被称为“违例控制机制”。
捕获程序错误最理想的时机是在编译阶段,这样能够完全避免错误的代码运行。但并不是全部的错误都能在编译期间侦测到,有些问题必须在运行期间解决。java

错误在运行期间发生时,咱们可能不知道具体应该怎样解决,但咱们清楚此时不能无论不顾地继续执行下去。此时应该作的事情是:程序员

  • 暂停程序的运行
  • 指出什么时候、何地发生了什么样的错误
  • 可能的话应处理此错误并恢复程序的执行

Java异常控制机制的做用流程:

  1. 异常产生
    首先程序引擎须要可以获知异常的产生。Java中预置了一系列基本的异常条件,如数组下标越界、空指针、被零除等等,这些异常是由JVM自动产生的(也被称为运行时异常,见后);另外一部分异常则是由Java代码(多是JDK的代码或开发人员本身编写的代码)产生的(也被称为checked异常,见后)。
    异常产生便是异常对象的实例化,该对象的类型一般就说明了异常条件的类型,实例化的异常对象中还会包含对异常条件的补充说明(message),以及异常发生时的线程调用栈信息(stacktrace)。
    在这个环节中,JAVA完成了对错误的描述,包括错误发生的时间、错误的类型(即异常对象的Class)、对错误的描述(message)和错误发生的位置(stacktrace)。编程

  2. 异常抛出
    异常抛出是JAVA程序流中的一种特殊流程,当异常产生后,JVM会中止继续执行后面的代码,并将异常对象抛出。抛出的异常对象会进入调用栈的上一层,若是异常对象没有被捕获,它会沿着调用栈的顺序逐层向上抛出,直至调用栈为空,此时该线程的运行也就完全终止了。
    异常的抛出解决了当前做用域可能不具有处理异常所需的信息的问题,将异常对象在调用栈中逐级向上传递,直至有能力处理异常的做用域将其捕获。api

  3. 异常捕获
    在异常对象逐级向上抛出的过程当中,若是调用栈中某一层有捕获该类型异常的逻辑,该异常对象便会被捕捉,异常被捕获后JVM会终止抛出异常对象的过程。数组

  4. 异常处理
    当异常对象被捕获后,JVM会执行捕获后的处理逻辑(处理逻辑是由程序员编写的)。当处理逻辑执行完成后,JVM会继续执行捕获了异常的做用域中接下来的代码(除非异常处理逻辑中将该异常继续抛出,或异常处理逻辑中产生了新的异常)。安全


try-catch-finally

前文所述的异常控制流程,在JAVA程序中以try-catch-finally结构实现:框架

 
图片.png
  1. try块也被称为“警惕区”,try块包裹的代码在执行过程若是产生异常,或其调用栈的下层中产生了异常并被抛至本层,则会被与此try块关联的catch命令尝试捕获。若异常产生于警惕区以外,则会直接向上层抛出。
  2. catch命令后的括号内指定但愿捕捉的异常对象类型(能够指定多个),若是产生或被抛至此层的异常对象是catch指定的异常类型(或其子类),则异常对象会被捕捉。上例中,全部Exception对象及其子类的对象在此处均会被捕获。
  3. 被捕获后,JVM会执行catch块中的代码,catch块中的代码可以访问被捕捉到的异常对象(即上例中的Exception e)。
    catch块中的代码仍然有可能产生异常,因此也能够在catch块中插入try-catch-finally。
  4. finally块为可选块,若是有,则不管是否有异常被抛出,JVM都会在try-catch块执行完成后执行finally块中的代码。

Exception与Error

前文所述的Java异常控制机制实际上并不只对“异常”起做用。除了咱们所说的异常(Exception)可以被产生、抛出和捕捉以外,还有另外一种类型“错误(Error)”。
Java中,Throwable是全部能够被抛出并捕获的类的父类。Throwable有两大子类,分别是Exception和Error。
Java官方并无给出Error和Exception的严格定义,而是将Error描述为“应用程序不该尝试捕捉处理的严重问题”,Exception则是“应用程序应该尝试捕捉处理的问题”。spa

咱们从几个例子看一下:线程

  • NoClassDefFoundError:JVM的ClassLoader在尝试加载某个类,但该类在Classpath中并不存在时会产生的错误。例如a.jar依赖b.jar中的某个类,若是咱们使用编译完成的a.jar时并无引入b.jar,编译器并不会发现问题(由于a.jar已经完成了编译,须要编译的代码中只使用了a.jar中的api,并无直接使用b.jar),但在运行时JVM找不到b.jar中被a所依赖的类,便会发生错误。
  • UnsupportedClassVersionError:当JVM尝试加载一个class但发现该class的版本并不被支持时产生的错误。例如咱们使用JDK1.8开发并编译一个类,但在JDK1.7的环境中运行时,便会发生此错误
  • OutOfMemoryError:当JVM内存不足,没法为一个对象分配内存时发生的错误,例如堆区内存溢出、Perm区内存溢出等。
  • StackOverFlowError:当程序的递归调用过深,致使线程调用栈溢出时发生的错误。
  • NoSuchFieldError/NoSuchMethodError:当JVM试图访问某个成员属性或某个方法时,发现目标不存在。通常都是因为class信息在运行时被改变致使的,多见于使用反射时。

经过上面的例子可以看出,Error通常都与程序自己的直接关系不大,更可能是因为环境致使的问题。并且Error发生后一般程序都没有再继续执行下去的可能性,因此Java官方将其定义为“应用程序不该尝试捕捉处理的严重问题”。3d


Exception的分类

Java将Exception分为两类,checked异常和unchecked异常,也被称为非运行时异常和运行时(runtime)异常。
RuntimeException是Exception的一个子类,RuntimeException的子类都属于unchecked异常(也就是运行时异常),其余全部的Exception都是checked异常(也就是非运行时异常)。

这两种异常的区别从字面上便可理解,checked表明“必须被check”,而unchecked表明“无须被check”:
Java要求checked异常必须被在代码编写阶段就调用者了解,unchecked异常则不用。若是一个方法中有可能产生checked异常,则Java编译器会要求该方法定义中必须加入throws定义,明确说明该方法可能会抛出某类checked异常。以下图:

 
图片.png

foo方法可能产生IOException(这是一种checked异常),因此bar方法在调用foo时,编译器会提示错误。此时能够在bar方法的定义行中加入throws:

public void bar() throws IOException 

也能够在bar方法内将IOException捕获处理:

 
图片.png

另外一个理解checked异常与unchecked异常区别的角度是:全部由JVM自动生成的异常都是unchecked异常,反之,由java程序主动生成的异常是checked异常。
例如:

 
图片.png

上图中f.createNewFile()方法可能会产生checked异常IOException,咱们看看File类的源码:

 
这里写图片描述

能够看到红框处,IOException异常是在代码中被主动抛出的,凡是这样在代码中主动抛出的异常,都是checked异常。

相应地,unchecked异常是JVM在运行时自动产生的,例以下图的方法,只要传入的参数b等于0,就会在运行时自动产生ArithmeticException:

 
图片.png
 
图片.png

代码中永远不须要这样写:

 
图片.png

异常处理的原则

异常处理的原则主要有三个:

  • 具体明确
  • 提前抛出
  • 延迟捕获

具体明确:
指抛出的异常应能经过异常类名和message准确说明异常的类型和产生异常的缘由。

咱们经过例子来看:

代码1:

 
图片.png

代码2:

 
图片.png

这两段代码的处理逻辑是相似的,均是在入参input1或input2为null或空串时抛出异常,但只有第二段符合“具体明确”的标准:
首先,第二段代码经过异常类型【IllegalArgumentException】明确了异常是因为传入了不合法的参数致使的;其次,在message中说明了具体是哪一个参数不合法,为何不合法。这样不只可以在查阅日志时快速知晓异常产生的缘由,也让上层的程序可以针对IllegalArgumentException这一特定类型的异常进行有针对性的捕捉和处理。
相比之下,第一段代码中抛出的异常就不够具体明确,异常类型Exception不具备说明性质,异常message也不够明确,上层程序难以处理,阅读日志时也难以快速定位。

提前抛出:

指应尽量早的发现并抛出异常,便于精肯定位问题。

一样经过例子来看:

代码1:

 
图片.png

代码2:

 
图片.png

在传入的filename为null时,这两段代码都会抛出异常,第一段代码抛出的异常是:

 
图片.png

第二段代码抛出的异常是:

 
图片.png

第一段代码抛出的异常是在标准Java类库【InputFileStream】中抛出的,这首先就提高了问题定位的难度,不过幸亏stacktrace中也打印出了前面的调用链,咱们能够在标准类库的调用者身上查找问题(能够定位到Test.java的第38行)。
同时NullPointerException是Java中信息量最少的(却也是最常遭遇且让人崩溃的)异常。它压根不提咱们最关心的事情:到底哪里是null。在稍微复杂一些的场景中(如一行代码中有多处均可能致使NullPointerException)会让人更加崩溃。

而相比之下第二段代码对filename提早进行了校验,并以IllegalArgumentException的形式抛出,这样在第一段代码中遇到的两个问题均可以获得解决,这即是提前抛出的好处。

延迟捕获:

指异常的捕获和处理应尽量延迟,让掌握更多信息的做用域来处理异常。

代码1:

 
图片.png

上面的代码中,readSomeFile方法将new FileInputStream处有可能产生的FileNotFoundException捕获,并将异常信息记录到了日志中。
这么作看起来彷佛没什么问题,但readSomeFile这个方法有多是一个通用的底层方法,会在各类业务场景下被调用,不一样的业务场景下,发生FileNotFoundException时的处理策略可能不同(例如某些场景要求记录异常并告警,某些场景会使用其余文件名重试),但readSomeFile方法并不知道本身所处的业务场景是什么样的,这一信息只有更上层的做用域才了解,因此在方法内部直接捕获并处理异常的作法就显得有问题了,程序将没法经过甄别业务场景来执行不一样的异常处理逻辑。

代码2:

 
图片.png

第二段代码看起来反而更加简单了,没有对FileNotFoundException加以处理,而是直接在方法定义中将其抛出。然而在上面所述的场景下,这种处理方式反而是正确的。将异常抛出交由掌握了足够多信息的上层调用者捕获,这样就能够根据异常产生所处的具体业务流程来进行不一样的处理。

例如咱们能够在一个业务逻辑中这样处理:

 
图片.png

同时在另外一个业务逻辑中这样处理:

 
图片.png

其余重要原则

  1. 不要让异常逃掉
    当一个异常在整个调用栈中的任意一层都没有被捕获,这个异常就“逃掉”了。这对于任何程序来讲都是一个灾难性的事件。
    对于B/S系统,从请求处理线程中逃掉的异常极可能会被B/S框架(如Struts/SpringMVC等)捕捉到。若是没有正确配置,这些逃掉的异常极可能就被框架“吃掉”了,即框架捕获了从业务代码层抛出的异常,且没有记录或没有完整记录异常信息。这样的异常来无影去无踪,彻底无迹可寻,堪称程序员的大敌。
    某些状况下,异常会被抛到中间件或容器(Tomcat/Jboss/Weblogic/Websphere等)层(多是没有使用B/S框架或B/S框架没有“吃掉”异常)。被中间件或容器捕获到的异常,通常状况下会被记录在中间件或容器本身的日志中(也有可能不会记),但问题在于,这种状况下,用户会看到中间件或容器提供的错误页,这些错误页基本没有用户友好型可言,并且有可能会把异常堆栈的信息直接显示在页面上,在开放性的系统中,暴露堆栈信息极有可能引起严重的安全问题。
    而在后台进程中,若是异常逃掉了,将会致使线程的退出。若是没有守护线程及时补充异常退出的线程,那么将有可能发生整个进程由于异常而停止的灾难性后果。
    因此说,在编程时应绝对避免异常“逃逸”的状况,对于B/S系统来讲,咱们能够在每一个Action中都加入try-catch块,捕获全部Exception,也能够利用B/S框架的特性来实现从Action层抛出的异常的统一处理(如Struts2和SpringMVC都有的拦截器机制)。对于后台进程来讲,能够利用try-catch块避免异常致使线程停止,也能够经过添加守护线程来及时补充因异常而退出的线程,同时还应使用Thread.setDefaultUncaughtExceptionHandler来确保未捕获异常的正确记录。

  2. 正确记录异常信息
    即在异常的stacktrace信息完整、未缺失的基础上,确保异常的stacktrace被正确记录到日志中

错误的作法:

 
图片.png

上面的5种处理全都是错误的,前两种将异常信息输出到了控制台而不是日志文件中。后三种错误的使用了log4j的error方法,均没有正确记录异常的stacktrace

正确的方法:

 
图片.png

注意应使用正确的error方法,传入两个参数,参数1是对异常的附加描述,参数2是未被篡改过的异常对象
在某些状况下,可能须要在处理异常后继续抛出,让上层捕获后继续处理,在这种状况下,须要注意抛出的异常对象未被篡改。

错误的:

 
图片.png

若是像上图这样写的话,下层的异常stacktrace会所有被吃掉。

正确的写法:

 
图片.png
相关文章
相关标签/搜索