我们为什么要做异常处理
- 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 错误:是严重的运行时环境问题,几乎肯定无法恢复。 一些示例是
OutOfMemoryError
,LinkageError
和StackOverflowError
。 它们通常会让程序崩溃或程序的一部分。 只有良好的日志练习才能帮助你确定错误的确切原因。
怎么处理异常?
我们所有处理异常的方式,都是为上面的第一点和第二点服务的,同时受第三点的约束。
对异常进行文档说明
如果方法上有抛出异常,则在方法注释中需要说明异常在说明情况下回抛出;
/**
* 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 的方式来处理,比如:NullPointerException
,IndexOutOfBoundsException
等等。 说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catchNumberFormatException
来实现。- 正例: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,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。 正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。