Java异常处理和最佳实践(含案例分析)
阿里妹导读
如何处理Java异常?作者查看了一些异常处理的规范,对 Java 异常处理机制有更深入的了解,并将自己的学习内容记录下来,希望对有同样困惑的同学提供一些帮助。
一、概述
了解Java异常的分类,什么是检查异常,什么是非检查异常 从字节码层面理解Java的异常处理机制,为什么finally块中的代码总是会执行 了解Java异常处理的不规范案例 了解Java异常处理的最佳实践 了解项目中的异常处理,什么时候抛出异常,什么时候捕获异常
二、java 异常处理机制
1、java 异常分类
Thorwable类(表示可抛出)是所有异常和错误的超类,两个直接子类为Error和Exception,分别表示错误和异常。 其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常, 这两种异常有很大的区别,也称之为非检查异常(Unchecked Exception)和检查异常(Checked Exception),其中Error类及其子类也是非检查异常。
检查异常和非检查异常
检查异常:也称为“编译时异常”,编译器在编译期间检查的那些异常。由于编译器“检查”这些异常以确保它们得到处理,因此称为“检查异常”。如果抛出检查异常,那么编译器会报错,需要开发人员手动处理该异常,要么捕获,要么重新抛出。除了RuntimeException之外,所有直接继承 Exception 的异常都是检查异常。 非检查异常:也称为“运行时异常”,编译器不会检查运行时异常,在抛出运行时异常时编译器不会报错,当运行程序的时候才可能抛出该异常。Error及其子类和RuntimeException 及其子类都是非检查异常。
说明:检查异常和非检查异常是针对编译器而言的,是编译器来检查该异常是否强制开发人员处理该异常:
检查异常导致异常在方法调用链上显式传递,而且一旦底层接口的检查异常声明发生变化,会导致整个调用链代码更改。 使用非检查异常不会影响方法签名,而且调用方可以自由决定何时何地捕获和处理异常
建议使用非检查异常让代码更加简洁,而且更容易保持接口的稳定性。
检查异常举例
非检查异常举例
自定义检查异常
自定义非检查异常
2、从字节码层面分析异常处理
try-catch-finally的本质
案例一:try-catch 字节码分析
如果在异常表中找到与 objectref 匹配的异常处理程序,PC 寄存器被重置到用于处理此异常的代码的位置,然后会清除当前帧的操作数堆栈,objectref 被推回操作数堆栈,执行继续。 如果在当前框架中没有找到匹配的异常处理程序,则弹出该栈帧,该异常会重新抛给上层调用的方法。如果当前帧表示同步方法的调用,那么在调用该方法时输入或重新输入的监视器将退出,就好像执行了监视退出指令(monitorexit)一样。 如果在所有栈帧弹出前仍然没有找到合适的异常处理程序,这个线程将终止。
异常表:异常表中用来记录程序计数器的位置和异常类型。如上图所示,表示的意思是:如果在 8 到 16 (不包括16)之间的指令抛出的异常匹配 MyCheckedException 类型的异常,那么程序跳转到16 的位置继续执行。
案例二:try-catch-finally 字节码分析
案例三:finally 块中的代码为什么总是会执行
案例四:finally 块中使用 return 字节码分析
public int getInt() {
int i = 0;
try {
i = 1;
return i;
} finally {
i = 2;
return i;
}
}
public int getInt2() {
int i = 0;
try {
i = 1;
return i;
} finally {
i = 2;
}
}
先分析一下 getInt() 方法的字节码:
try-with-resources 的本质
/**
* 打包多个文件为 zip 格式
*
* @param fileList 文件列表
*/
public static void zipFile(List<File> fileList) {
// 文件的压缩包路径
String zipPath = OUT + "/打包附件.zip";
// 获取文件压缩包输出流
try (OutputStream outputStream = new FileOutputStream(zipPath);
CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream)) {
for (File file : fileList) {
// 获取文件输入流
InputStream fileIn = new FileInputStream(file);
// 使用 common.io中的IOUtils获取文件字节数组
byte[] bytes = IOUtils.toByteArray(fileIn);
// 写入数据并刷新
zipOut.putNextEntry(new ZipEntry(file.getName()));
zipOut.write(bytes, 0, bytes.length);
zipOut.flush();
}
} catch (FileNotFoundException e) {
System.out.println("文件未找到");
} catch (IOException e) {
System.out.println("读取文件异常");
}
}
可以看到在 try() 的括号中定义需要关闭的资源,实际上这是Java的一种语法糖,查看编译后的代码就知道编译器为我们做了什么,下面是反编译后的代码:
public static void zipFile(List<File> fileList) {
String zipPath = "./打包附件.zip";
try {
OutputStream outputStream = new FileOutputStream(zipPath);
Throwable var3 = null;
try {
CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
Throwable var5 = null;
try {
ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream);
Throwable var7 = null;
try {
Iterator var8 = fileList.iterator();
while(var8.hasNext()) {
File file = (File)var8.next();
InputStream fileIn = new FileInputStream(file);
byte[] bytes = IOUtils.toByteArray(fileIn);
zipOut.putNextEntry(new ZipEntry(file.getName()));
zipOut.write(bytes, 0, bytes.length);
zipOut.flush();
}
} catch (Throwable var60) {
var7 = var60;
throw var60;
} finally {
if (zipOut != null) {
if (var7 != null) {
try {
zipOut.close();
} catch (Throwable var59) {
var7.addSuppressed(var59);
}
} else {
zipOut.close();
}
}
}
} catch (Throwable var62) {
var5 = var62;
throw var62;
} finally {
if (checkedOutputStream != null) {
if (var5 != null) {
try {
checkedOutputStream.close();
} catch (Throwable var58) {
var5.addSuppressed(var58);
}
} else {
checkedOutputStream.close();
}
}
}
} catch (Throwable var64) {
var3 = var64;
throw var64;
} finally {
if (outputStream != null) {
if (var3 != null) {
try {
outputStream.close();
} catch (Throwable var57) {
var3.addSuppressed(var57);
}
} else {
outputStream.close();
}
}
}
} catch (FileNotFoundException var66) {
System.out.println("文件未找到");
} catch (IOException var67) {
System.out.println("读取文件异常");
}
}
JDK1.7开始,java引入了 try-with-resources 声明,将 try-catch-finally 简化为 try-catch,在编译时会进行转化为 try-catch-finally 语句,我们就不需要在 finally 块中手动关闭资源。
try 块没有发生异常时,自动调用 close 方法, try 块发生异常,然后自动调用 close 方法,如果 close 也发生异常,catch 块只会捕捉 try 块抛出的异常,close 方法的异常会在catch 中通过调用 Throwable.addSuppressed 来压制异常,但是你可以在catch块中,用 Throwable.getSuppressed 方法来获取到压制异常的数组。
三、java 异常处理不规范案例
捕获
捕获异常的时候不区分异常类型 捕获异常不完全,比如该捕获的异常类型没有捕获到
try{
……
} catch (Exception e){ // 不应对所有类型的异常统一捕获,应该抽象出业务异常和系统异常,分别捕获
……
}
异常信息丢失 异常信息转译错误,比如在抛出异常的时候将业务异常包装成了系统异常 吃掉异常 不必要的异常包装 检查异常传递过程中不适用非检查检异常包装,造成代码被throws污染
try{
……
} catch (BIZException e){
throw new BIZException(e); // 重复包装同样类型的异常信息
} catch (Biz1Exception e){
throw new BIZException(e.getMessage()); // 没有抛出异常栈信息,正确的做法是throw new BIZException(e);
} catch (Biz2Exception e){
throw new Exception(e); // 不能使用低抽象级别的异常去包装高抽象级别的异常,这样在传递过程中丢失了异常类型信息
} catch (Biz3Exception e){
throw new Exception(……); // 异常转译错误,将业务异常直接转译成了系统异常
} catch (Biz4Exception e){
…… // 不抛出也不记Log,直接吃掉异常
} catch (Exception e){
throw e;
}
处理
重复处理 处理方式不统一 处理位置分散
try{
try{
try{
……
} catch (Biz1Exception e){
log.error(e); // 重复的LOG记录
throw new e;
}
try{
……
} catch (Biz2Exception e){
…… // 同样是业务异常,既在内层处理,又在外层处理
}
} catch (BizException e){
log.error(e); // 重复的LOG记录
throw e;
}
} catch (Exception e){
// 通吃所有类型的异常
log.error(e.getMessage(),e);
}
四、java 异常处理规范案例
1、阿里巴巴Java异常处理规约
【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。 【推荐】定义时区分unchecked / checked 异常,避免直接使用RuntimeException抛出,更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。
后面的章节我将根据自己的思考,说明如何定义异常,如何抛出异常,如何处理异常,接着往下看。
2、异常处理最佳实践
logger.error("说明信息,异常信息:{}", e.getMessage(), e)
throw MyException("my exception", e);
9、自定义异常尽量不要使用检查异常。
五、项目中的异常处理实践
1、如何自定义异常
能够将错误代码和正常代码分离 能够在调用堆栈上传递异常 能够将异常分组和区分
在Java异常体系中定义了很多的异常,这些异常通常都是技术层面的异常,对于应用程序来说更多出现的是业务相关的异常,比如用户输入了一些不合法的参数,用户没有登录等,我们可以通过异常来对不同的业务问题进行分类,以便我们排查问题,所以需要自定义异常。那我们如何自定义异常呢?前面已经说了,在应用程序中尽量不要定义检查异常,应该定义非检查异常(运行时异常)。
业务异常:用户能够看懂并且能够处理的异常,比如用户没有登录,提示用户登录即可。 系统异常:用户看不懂需要程序员处理的异常,比如网络连接超时,需要程序员排查相关问题。
下面是我设想的对于应用程序中的异常体系分类:
/**
* 异常信息枚举类
*
*/
public enum ErrorCode {
/**
* 系统异常
*/
SYSTEM_ERROR("A000", "系统异常"),
/**
* 业务异常
*/
BIZ_ERROR("B000", "业务异常"),
/**
* 没有权限
*/
NO_PERMISSION("B001", "没有权限"),
;
/**
* 错误码
*/
private String code;
/**
* 错误信息
*/
private String message;
ErrorCode(String code, String message) {
this.code = code;
this.message = message;
}
/**
* 获取错误码
*
* @return 错误码
*/
public String getCode() {
return code;
}
/**
* 获取错误信息
*
* @return 错误信息
*/
public String getMessage() {
return message;
}
/**
* 设置错误码
*
* @param code 错误码
* @return 返回当前枚举
*/
public ErrorCode setCode(String code) {
this.code = code;
return this;
}
/**
* 设置错误信息
*
* @param message 错误信息
* @return 返回当前枚举
*/
public ErrorCode setMessage(String message) {
this.message = message;
return this;
}
}
自定义系统异常类,其他类型的异常类似,只是异常的类名不同,如下代码所示:
/**
* 系统异常类
*
*/
public class SystemException extends RuntimeException {
private static final long serialVersionUID = 8312907182931723379L;
/**
* 错误码
*/
private String code;
/**
* 构造一个没有错误信息的 <code>SystemException</code>
*/
public SystemException() {
super();
}
/**
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 SystemException
*
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
*/
public SystemException(Throwable cause) {
super(cause);
}
/**
* 使用错误信息 message 构造 SystemException
*
* @param message 错误信息
*/
public SystemException(String message) {
super(message);
}
/**
* 使用错误码和错误信息构造 SystemException
*
* @param code 错误码
* @param message 错误信息
*/
public SystemException(String code, String message) {
super(message);
this.code = code;
}
/**
* 使用错误信息和 Throwable 构造 SystemException
*
* @param message 错误信息
* @param cause 错误原因
*/
public SystemException(String message, Throwable cause) {
super(message, cause);
}
/**
* @param code 错误码
* @param message 错误信息
* @param cause 错误原因
*/
public SystemException(String code, String message, Throwable cause) {
super(message, cause);
this.code = code;
}
/**
* @param errorCode ErrorCode
*/
public SystemException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
/**
* @param errorCode ErrorCode
* @param cause 错误原因
*/
public SystemException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.code = errorCode.getCode();
}
/**
* 获取错误码
*
* @return 错误码
*/
public String getCode() {
return code;
}
}
2、如何使用异常
throw new BizException(ErrorCode.NO_PERMISSION);
什么时候抛出业务异常,什么时候抛出系统异常?
/**
* rpc 异常类
*/
public class RpcException extends SystemException {
private static final long serialVersionUID = -9152774952913597366L;
/**
* 构造一个没有错误信息的 <code>RpcException</code>
*/
public RpcException() {
super();
}
/**
* 使用指定的 Throwable 和 Throwable.toString() 作为异常信息来构造 RpcException
*
* @param cause 错误原因, 通过 Throwable.getCause() 方法可以获取传入的 cause信息
*/
public RpcException(Throwable cause) {
super(cause);
}
/**
* 使用错误信息 message 构造 RpcException
*
* @param message 错误信息
*/
public RpcException(String message) {
super(message);
}
/**
* 使用错误码和错误信息构造 RpcException
*
* @param code 错误码
* @param message 错误信息
*/
public RpcException(String code, String message) {
super(code, message);
}
/**
* 使用错误信息和 Throwable 构造 RpcException
*
* @param message 错误信息
* @param cause 错误原因
*/
public RpcException(String message, Throwable cause) {
super(message, cause);
}
/**
* @param code 错误码
* @param message 错误信息
* @param cause 错误原因
*/
public RpcException(String code, String message, Throwable cause) {
super(code, message, cause);
}
/**
* @param errorCode ErrorCode
*/
public RpcException(ErrorCode errorCode) {
super(errorCode);
}
/**
* @param errorCode ErrorCode
* @param cause 错误原因
*/
public RpcException(ErrorCode errorCode, Throwable cause) {
super(errorCode, cause);
}
}
这个 RpcException 所有的构造方法都是调用的父类 SystemExcepion 的方法,所以这里不再赘述。定义好了异常后接下来是处理 rpc 调用的异常处理逻辑,调用 rpc 服务可能会发生 ConnectException 等网络异常,我们并不需要在调用的时候捕获异常,而是应该在最上层捕获并处理异常,调用 rpc 的处理demo代码如下:
private Object callRpc() {
Result<Object> rpc = rpcDemo.rpc();
log.info("调用第三方rpc返回结果为:{}", rpc);
if (Objects.isNull(rpc)) {
return null;
}
if (!rpc.getSuccess()) {
throw new RpcException(ErrorCode.RPC_ERROR.setMessage(rpc.getMessage()));
}
return rpc.getData();
}
rpc 接口全局异常处理
/**
* Result 结果类
*
*/
public class Result<T> implements Serializable {
private static final long serialVersionUID = -1525914055479353120L;
/**
* 错误码
*/
private final String code;
/**
* 提示信息
*/
private final String message;
/**
* 返回数据
*/
private final T data;
/**
* 是否成功
*/
private final Boolean success;
/**
* 构造方法
*
* @param code 错误码
* @param message 提示信息
* @param data 返回的数据
* @param success 是否成功
*/
public Result(String code, String message, T data, Boolean success) {
this.code = code;
this.message = message;
this.data = data;
this.success = success;
}
/**
* 创建 Result 对象
*
* @param code 错误码
* @param message 提示信息
* @param data 返回的数据
* @param success 是否成功
*/
public static <T> Result<T> of(String code, String message, T data, Boolean success) {
return new Result<>(code, message, data, success);
}
/**
* 成功,没有返回数据
*
* @param <T> 范型参数
* @return Result
*/
public static <T> Result<T> success() {
return of("00000", "成功", null, true);
}
/**
* 成功,有返回数据
*
* @param data 返回数据
* @param <T> 范型参数
* @return Result
*/
public static <T> Result<T> success(T data) {
return of("00000", "成功", data, true);
}
/**
* 失败,有错误信息
*
* @param message 错误信息
* @param <T> 范型参数
* @return Result
*/
public static <T> Result<T> fail(String message) {
return of("10000", message, null, false);
}
/**
* 失败,有错误码和错误信息
*
* @param code 错误码
* @param message 错误信息
* @param <T> 范型参数
* @return Result
*/
public static <T> Result<T> fail(String code, String message) {
return of(code, message, null, false);
}
/**
* 获取错误码
*
* @return 错误码
*/
public String getCode() {
return code;
}
/**
* 获取提示信息
*
* @return 提示信息
*/
public String getMessage() {
return message;
}
/**
* 获取数据
*
* @return 返回的数据
*/
public T getData() {
return data;
}
/**
* 获取是否成功
*
* @return 是否成功
*/
public Boolean getSuccess() {
return success;
}
}
在编写 aop 代码之前需要先导入 spring-boot-starter-aop 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
RpcGlobalExceptionAop 代码如下:
/**
* rpc 调用全局异常处理 aop 类
*
*/
public class RpcGlobalExceptionAop {
/**
* execution(* com.xyz.service ..*.*(..)):表示 rpc 接口实现类包中的所有方法
*/
public void pointcut() {}
public Object handleException(ProceedingJoinPoint joinPoint) {
try {
//如果对传入对参数有修改,那么需要调用joinPoint.proceed(Object[] args)
//这里没有修改参数,则调用joinPoint.proceed()方法即可
return joinPoint.proceed();
} catch (BizException e) {
// 对于业务异常,应该记录 warn 日志即可,避免无效告警
log.warn("全局捕获业务异常", e);
return Result.fail(e.getCode(), e.getMessage());
} catch (RpcException e) {
log.error("全局捕获第三方rpc调用异常", e);
return Result.fail(e.getCode(), e.getMessage());
} catch (SystemException e) {
log.error("全局捕获系统异常", e);
return Result.fail(e.getCode(), e.getMessage());
} catch (Throwable e) {
log.error("全局捕获未知异常", e);
return Result.fail(e.getMessage());
}
}
}
aop 中 @Pointcut 的 execution 表达式配置说明:
execution(public * *(..)) 定义任意公共方法的执行
execution(* set*(..)) 定义任何一个以"set"开始的方法的执行
execution(* com.xyz.service.AccountService.*(..)) 定义AccountService 接口的任意方法的执行
execution(* com.xyz.service.*.*(..)) 定义在service包里的任意方法的执行
execution(* com.xyz.service ..*.*(..)) 定义在service包和所有子包里的任意类的任意方法的执行
execution(* com.test.spring.aop.pointcutexp…JoinPointObjP2.*(…)) 定义在pointcutexp包和所有子包里的JoinPointObjP2类的任意方法的执行
http 接口全局异常处理
基于请求转发的方式处理异常; 基于异常处理器的方式处理异常; 基于过滤器的方式处理异常。
基于请求转发的方式:真正的全局异常处理。
BasicExceptionController
基于异常处理器的方式:不是真正的全局异常处理,因为它处理不了过滤器等抛出的异常。
@ExceptionHandler @ControllerAdvice+@ExceptionHandler SimpleMappingExceptionResolver HandlerExceptionResolver
基于过滤器的方式:近似全局异常处理。它能处理过滤器及之后的环节抛出的异常。
Filter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
通过 @ControllerAdvice+@ExceptionHandler 实现基于异常处理器的http接口全局异常处理:
/**
* http 接口异常处理类
*/
public class HttpExceptionHandler {
/**
* 处理业务异常
* @param request 请求参数
* @param e 异常
* @return Result
*/
public Object bizExceptionHandler(HttpServletRequest request, BizException e) {
log.warn("业务异常:" + e.getMessage() , e);
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 处理系统异常
* @param request 请求参数
* @param e 异常
* @return Result
*/
public Object systemExceptionHandler(HttpServletRequest request, SystemException e) {
log.error("系统异常:" + e.getMessage() , e);
return Result.fail(e.getCode(), e.getMessage());
}
/**
* 处理未知异常
* @param request 请求参数
* @param e 异常
* @return Result
*/
public Object unknownExceptionHandler(HttpServletRequest request, Throwable e) {
log.error("未知异常:" + e.getMessage() , e);
return Result.fail(e.getMessage());
}
}
在 HttpExceptionHandler 类中,@RestControllerAdvice = @ControllerAdvice + @ResponseBody ,如果有其他的异常需要处理,只需要定义@ExceptionHandler注解的方法处理即可。
六、总结
http://javainsimpleway.com/exception-handling-best-practices/
https://www.infoq.com/presentations/effective-api-design/
https://docs.oracle.com/javase/tutorial/essential/exceptions/advantages.html
微信扫码关注该文公众号作者