实战:java基础系列之【Exception:如何处理异常】

概叙
异常定义(Error/Exception)
异常:在Java语言中, 将程序执行中发生的不正常情况称为“异常” 。 (开发过程中的语法错误和逻辑错误不是异常) Java程序在执行过程中所发生的异常事件可分为两类:
- Error: Java虚拟机无法解决的严重问题。 如: JVM系统内部错误、 资源耗尽等严重情况。 比如: StackOverflowError和OOM。 一般不编写针对性的代码进行处理。
- Exception: 其它因编程错误或偶然的外在因素导致的一般性问题, 可以使用针对性的代码进行处理。 例如: 1、空指针访问 2、试图读取不存在的文件 3、网络连接中断 4、数组角标越界
对于这些错误, 一般有两种解决方法:
- 一种是遇到错误就终止程序的运行。
- 一种方法是由程序员在编写程序时, 就考虑到错误的检测、 错误消息的提示, 以及错误的处理。

要理解 Java 异常处理是如何工作的,你需要掌握以下三种类型的异常:受检异常(编译器检查)、运行时异常(编译器不检查、业务代码bug)、错误(不需要处理)
- 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这些异常在编译时强制要求程序员处理。
- 例如,如果程序从磁盘读入数据,而系统找不到含有数据的文件,将会发生受检异常,这个异常的类名是FileNotFoundException,发生的原因可能是用户给程序提供了一个错误的文件名。
- 非检查性异常/运行时异常:运行时异常的所有类都是RuntimeException的子类。 这些异常在编译时不强制要求处理,通常是由程序中的错误引起的。
- 例如 NullPointerException、ArrayIndexOutOfBoundsException 等,这类异常可以选择处理,但并非强制要求。
- 例如,数组下标越界导致IndexOutOfBoundsException异常,被0除导致ArithmeticException异常。虽然可以添加代码来处理运行时异常,但一般只需要修改程序中的错误即可。
- 错误Error: 错误是标准类Error或其后代类的一个对象。程序中无法处理的错误,表示运行应用程序中出现了严重的错误。错误不是异常,而是脱离程序员控制的问题,错误在代码中通常被忽略。它是指发生了不正确的情况,
- 如内存溢出。这些情况都比较严重,一般很难通过程序处理。例如OOM(OutOfMemoryError),简单的解决方法就是通过设置Java程序的启动参数来设置运行时内存大小。当栈溢出时,一个错误就发生了StackOverFlowError,它们在编译也检查不到的。
- Error错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的!
为什么要处理异常
目的只有一个:为了更加健壮的程序(本质是少背锅)。
事实上,领导是不会拿自己的脑袋宣言的:“我们的程序绝不存在任何一个 bug。”但当程序出现 bug 的时候,领导会毫不犹豫地选择让程序员背锅。
为了让自己少背锅,我们可以这样做:
- 在编码阶段合理使用异常处理机制,并记录日志以备后续分析
- 在测试阶段进行大量有效的测试,在用户发现错误之前发现错误
Java中异常提供了一种识别及响应错误情况的一致性机制,有效地异常处理能使程序更加健壮、易于调试。
在有效使用异常的情况下,异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪“抛出,异常信息回答了“为什么“会抛出,如果你的异常没有回答以上全部问题,那么可能你没有很好地使用它们。
异常处理方式
参考:

在Springboot工程中:@ControllerAdvice和@ExceptionHandler实现全局异常处理。
异常处理建议
可参考:
4)使用异常还是错误码返回
在分布式微服务应用中,系统调用依赖关系复杂,系统内部之间抛异常传递,系统与系统之间使用错误码返回,这样能够清晰看到异常错误原因、定位问题。
5)统一错误码
错误码本身不算是代码问题,不过基于整个组织和工程的可维护性来说,可以将错误码不符合规范作为一种错误加以避免。方法:对错误码进行可控的管理和遵循规范使用。可以使用公共文档维护, 也可以开发错误码管理系统来避免相同的错误码。
1. 统一的系统范围的异常类

不要针对每种异常类型创建单独的类,而是只创建一个,并使它继承RuntimeException。这可以减少类的数量,并移除你不会去处理的需要声明的异常。
我知道你在想什么:如何告诉异常处理程序,这些是否是同一类型?如何跟踪特定类型的属性?继续读下去。
2. 为错误代码使用枚举
大多数开发者会把导致异常的原因放到消息中,出现异常时,查看日志文件即可。但是这也有一些缺点:
消息不能被翻译
消息不能很容易地映射为易读的文本
无法从程序方面对消息进行检查
由于每个开发者的语言习惯不一样,同样的错误可能有不同的描述形式。
更好的办法是使用枚举来表示异常的类型。针对每种错误创建一个枚举,并使枚举实现一个ErrorCode接口,然后将它引用为异常中的一个字段。
当抛出异常时,只需在适当的枚举中传递即可。throw new SystemException(PaymentCode.CREDIT_CARD_EXPIRED);
现在,当你需要测试具体情况时,只需比较异常代码即可。
catch (SystemException e) {
if (e.getErrorCode() == PaymentCode.CREDIT_CARD_EXPIRED) {
...
}
}
在资源包中使用错误代码作为关键字进行检索,即可取回易读的、国际化的文本。
1. public class
2.
3. public static void
4. System.out.println(getUserText(ValidationCode.VALUE_TOO_SHORT));
5. }
6.
7. public static
8. if (errorCode == null) {
9. return null;
10. }
11. "__"
12. "com.northconcepts.exception.example.exceptions");
13. return
14. }
15.
16. }
public class SystemExceptionExample3 {
public static void main(String[] args) {
System.out.println(getUserText(ValidationCode.VALUE_TOO_SHORT));
}
public static String getUserText(ErrorCode errorCode) {
if (errorCode == null) {
return null;
}
String key = errorCode.getClass().getSimpleName() + "__" + errorCode;
ResourceBundle bundle = ResourceBundle.getBundle("com.northconcepts.exception.example.exceptions");
return bundle.getString(key);
}
}
3. 在枚举类型中添加错误代码
在某些情况下,一个数字形式的错误代码可以对应一个异常,例如HTTP响应。在这种情况下,在ErrorCode接口中添加一个getNumber方法,并在每个枚举类型中实现它。
public enum PaymentCode implements ErrorCode {
SERVICE_TIMEOUT(101),
CREDIT_CARD_EXPIRED(102),
AMOUNT_TOO_HIGH(103),
INSUFFICIENT_FUNDS(104);
private final int number;
private PaymentCode(int number) {
this.number = number;
}
@Override
public int getNumber() {
return number;
}
}
这些数字可以是全局唯一的,或者每个枚举类型只对应一个数字。你甚至可以使用隐式ordinal()方法,或者从一个文件/数据库中加载一些数字。
4. 将动态字段添加到异常处理中
好的异常处理也意味着记录相关数据,而不仅仅是堆栈跟踪。这样做会节省你大量用于诊断和重现错误的时间。当你的程序停止工作时,也无需客户告诉你,他们做了什么。
做到这一点最简单的方法是在异常处理中增加一个java.util.Map字段。该字段主要作用是保留所有异常相关的数据。如果你使用fluent interface(连贯接口)模式,你还需要添加一个通用的setter方法。
抛出异常,并带有相关的数据,类似于下面的形式:
throw new SystemException(ValidationCode.VALUE_TOO_SHORT)
.set("field", field)
.set("value", value)
.set("min-length", MIN_LENGTH);
5. 防止不必要的嵌套
长且多余的堆栈跟踪,对谁都没有好处。更糟的是,它们浪费你的时间和资源。当重新抛出异常时,调用一个静态封装方法,而不是异常的构造函数。封装方法将决定何时嵌套异常,以及何时返回原来的实例。
1. public static
2. if (exception instanceof
3. SystemException se = (SystemException)exception;
4. if (errorCode != null
5. return new
6. }
7. return
8. else
9. return new
10. }
11. }
12.
13. public static
14. return wrap(exception, null);
15. }
public static SystemException wrap(Throwable exception, ErrorCode errorCode) {
if (exception instanceof SystemException) {
SystemException se = (SystemException)exception;
if (errorCode != null && errorCode != se.getErrorCode()) {
return new SystemException(exception.getMessage(), exception, errorCode);
}
return se;
} else {
return new SystemException(exception.getMessage(), exception, errorCode);
}
}
public static SystemException wrap(Throwable exception) {
return wrap(exception, null);
}
重新抛出异常的代码类似于:
} catch (IOException e) {
throw SystemException.wrap(e).set("fileName", fileName);
}
6. finally执行扫尾工作
当抛出异常的时候,剩余的代码就会终止执行,这时候一些资源就需要主动回收。Java 的解决方案就是 finally
子句——不管异常有没有被捕获,finally
子句里的代码都会执行。
所以finally
里面执行关闭连接、关闭流等释放资源操作。
public static void main(String[] args) {
InputStream is = null;
try {
is = new FileInputStream("文件.txt");
int b;
while ((b = is.read()) != -1) {
。。。
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 不要说这里丑,那是你不知道它的香
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
7. catch 原始异常,不要自己吃了;也不要瞎抛异常。
InputStream is = null;
try {
is = new FileInputStream("文件.txt");
} catch (Exception e) {
e.printStackTrace();
}
上面的反例把 FileNotFoundException吃了却抛出泛化的 Exception。这样不负责的吃异常,非常不道德,codereview第一个被打回。
public static void main(String[] args) throws IOException {
try (InputStream is = new FileInputStream("文件.txt")) {
}catch (IOException e) {
e.printStackTrace();
throw e;
}
}
java.io.FileNotFoundException: 文件.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at learning.Test.main(Test.java:10)
Exception in thread "main" java.io.FileNotFoundException: 文件.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at learning.Test.main(Test.java:10)
不要重复打印异常"打印堆栈后再抛出异常" ,当异常发生时打印它,然后重新抛出它,以便调用者能够适当地处理它。就像上面这段代码一样。
8. 不要用异常处理机制代替判断
try {
// 执行事务
...
} catch (SQLException e) {
if (e.getErrorCode() == 1213) { // 1213 代表死锁错误代码
// 死锁检测,进行重试
retryTransaction();
} else {
// 其他异常处理
throw e;
}
}
前面死锁案例中,在catch里面做死锁检查和重试,尽量还是不要catch里面做过多的业务操作。
能拆出来,尽量拆出来。
8. 不要盲目地过早捕获异常
如果盲目地过早捕获异常的话,通常会导致更严重的错误和其他异常。请看下面的例子。
InputStream is = null;
try {
is = new FileInputStream("文件.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
int b;
try {
while ((b = is.read()) != -1) {
}
} catch (IOException e) {
e.printStackTrace();
}
finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
java.io.FileNotFoundException: 文件.txt (系统找不到指定的文件。)
at java.io.FileInputStream.open0(Native Method)
at java.io.FileInputStream.open(FileInputStream.java:195)
at java.io.FileInputStream.<init>(FileInputStream.java:138)
at java.io.FileInputStream.<init>(FileInputStream.java:93)
at learning.Test.main(Test.java:12)
Exception in thread "main" java.lang.NullPointerException
at learning.Test.main(Test.java:28)
假如文件没有找到的话,InputStream
的对象引用 is 就为 null
,新的 NullPointerException
就会出现。NullPointerException
并不是程序出现问题的本因,但实际上它出现了,无形当中干扰了我们的视线。正确的做法是延迟捕获异常,让程序在第一个异常捕获后就终止执行。
9. 循环里面尽量不要捕捉异常(视情况而定)
代码片段 A
long a = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
try {
String str = null;
String[] strs = str.split(",");
} catch (NullPointerException e) {
}
}
long b = System.currentTimeMillis();
System.out.println(b - a);
代码片段 B
long a = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
String str = null;
if (str != null) {
String[] strs = str.split(",");
}
}
long b = System.currentTimeMillis();
System.out.println(b - a);
100000 次的循环,有异常尽早中断循环;如果是while(true)的这种,容易死循环。
代码片段 A(异常处理机制)执行的时间大概需要 1983 毫秒;
代码片段 B(正常判断)执行的时间大概只需要 1 毫秒。
这样的比较虽然不够精确,但足以说明问题。
10. 使用一个有Web控制面板的中央记录器
根据你的情况,访问产品日志可能会相当麻烦。因为这可能会涉及到多个中间人。
如果你在一个多服务器环境中,事情会更糟。找到出问题的服务器,并确定该问题只影响这一台服务器,这可能相当令人头痛。
我的建议是:
- 将日志聚合到某一位置,最好是数据库
- 允许从Web浏览器访问该数据库
有很多方法可以做到这一点,比如:日志收集器、远程记录器、JMX代理、系统监控软件等,你甚至可以自己构建一个。一旦你拥有它,你将能够:
- 在几秒钟内解决问题。
- 每个异常都有一个URL对应。
- 维护人员无需通过其他人的帮助即可确定异常原因
- 防止测试人员为同样的错误创建多个tickets。
- 为企业节省资金
- 节省你的时间,不会影响你的周末度假