Java异常处理最佳实践

59059b1620f8a_610.jpg

我们为什么要做异常处理

  • 1、给请求端明确的操作指导。
  • 2、正确记录系统异常时的完整场景,包括代码的调用过程、出错点和数据,便于实施针对异常情况的后续处理;

异常处理的一些约束

异常的处理依赖编程语言的机制,所以也会有一些语言层面的约束

  • 3、异常处理影响性能:异常处理的性能成本非常高,每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢,抛出一个异常又会消耗1~5ms,当一个异常在应用的多个层级之间传递时,会拖累整个应用的性能。

异常处理的原则

  • 对于公司外的http/api开放接口必须使用“错误码”;

  • 应用内部推荐异常抛出;

  • 跨应用间RPC调用优先考虑使用Result方式,封装isSuccess()方法、“错误码”、“错误简短信息”。

    关于RPC方法返回方式使用Result方式的理由:

    1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。

    2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。

  • “早throw晚catch”原则:你应该尽快抛出(throw)异常,并尽可能晚地捕获(catch)它。 你应该等到你有足够的信息来妥善处理它。

异常的相关概念

  • checked exception 检查性异常 :是必须在在方法的throws子句中声明的异常。它们扩展了异常,旨在成为一种“在你面前”的异常类型。JAVA希望你能够处理它们,因为它们以某种方式依赖于程序之外的外部因素。检查的异常表示在正常系统操作期间可能发生的预期问题。## 标题 ## 当你尝试通过网络或文件系统使用外部系统时,通常会发生这些异常。 大多数情况下,对检查性异常的正确响应应该是稍后重试,或者提示用户修改其输入。

  • unchecked Exception 非检查性异常 :是不需要在throws子句中声明的异常。 由于程序错误,JVM并不会强制你处理它们,因为它们大多数是在运行时生成的。 它们扩展了RuntimeException。 最常见的例子是NullPointerException [相当可怕..是不是?]。 未经检查的异常可能不应该重试,正确的操作通常应该是什么都不做,并让它从你的方法和执行堆栈中出来。 在高层次的执行中,应该记录这种类型的异常。

  • error 错误:是严重的运行时环境问题,几乎肯定无法恢复。 一些示例是OutOfMemoryErrorLinkageErrorStackOverflowError。 它们通常会让程序崩溃或程序的一部分。 只有良好的日志练习才能帮助你确定错误的确切原因。

怎么处理异常?

我们所有处理异常的方式,都是为上面的第一点和第二点服务的,同时受第三点的约束。

对异常进行文档说明

如果方法上有抛出异常,则在方法注释中需要说明异常在说明情况下回抛出;

/**
 * This method does something extremely useful ...
 *
 * @param input
 * @throws MyBusinessException if ... happens
 */
public void doSomething(String input) throws MyBusinessException {
    ...
}

对抛出的异常进行明确的说明&包装异常时不要抛弃原始异常

private void fireException(){
    try {
        throw new NullPointerException();
    } catch (NullPointerException e) {
        // 异常信息中提供尽可能多的对定位问题有帮助的信息
        throw new BizException("没有找到匹配商品,id="+prodId,e);
    }
}

使用标准异常(JDK&Spring)

如果使用内建的异常可以解决问题,就不要定义自己的异常。Java API 提供了上百种针对不同情况的异常类型,在开发中首先尽可能使用 Java API 提供的异常,如果标准的异常不能满足你的要求,这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。

NullPointerException 处理

  • Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerExceptionIndexOutOfBoundsException等等。 说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。
    • 正例:if (obj != null) {…}
    • 反例:try { obj.method(); } catch (NullPointerException e) {…}
  • 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。
    • 反例:public int f() { return Integer对象}, 如果为null,自动解箱抛NPE。
  • 远程调用返回对象时,一律要求进行空指针判断,防止NPE。
  • 使用JDK8的Optional类来防止NPE问题。

SpringMVC统一处理异常

@ControllerAdvice // 通过注解拦截所有@RequestMapping
public class GlobalExceptionHandler {
    ...
}

只从方法中抛出相关异常

相关性对于保持应用程序清洁非常重要。 一种尝试读取文件的方法; 如果抛出NullPointerException,那么它不会给用户任何相关的信息。 相反,如果这种异常被包裹在自定义异常中,则会更好。 NoSuchFileFoundException则对该方法的用户更有用。


一些异常处理的典型反例

不要捕获 Throwable

Throwable 是所有异常和错误的超类。你可以在 catch 子句中使用它,但是你永远不应该这样做!

如果在 catch 子句中使用 Throwable ,它不仅会捕获所有异常,也将捕获所有的错误。JVM 抛出错误,指出不应该由应用程序处理的严重问题。 典型的例子是 OutOfMemoryError 或者 StackOverflowError。两者都是由应用程序控制之外的情况引起的,无法处理。

所以,最好不要捕获 Throwable ,除非你确定自己处于一种特殊的情况下能够处理错误。

在调用RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。 说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException。什么情况会抛出NoSuchMethodError呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。

不要忽略异常

很多时候,开发者很有自信不会抛出异常,因此写了一个catch块,但是没有做任何处理或者记录日志。

public void doNotIgnoreExceptions() {
    try {
        // do something
    } catch (NumberFormatException e) {
        // this will never happen
    }
}

不要记录并抛出异常

不要记录并抛出异常,这会给同一个异常输出多条日志。

try {
    new Long("xyz");
} catch (NumberFormatException e) {
    log.error(e);
    throw e;
}

不要使用异常控制程序的流程

不应该使用异常控制应用的执行流程,例如,本应该使用if语句进行条件判断的情况下,你却使用异常处理,这是非常不好的习惯,会严重影响应用的性能。

不要直接catch大段代码,并抛出大异常

catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。 说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。 正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。

你可能感兴趣的:(Java异常处理最佳实践)