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 = ; try { i = 1; return i; } finally { i = 2; return i; }}
public int getInt2() { int i = ; 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, , 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, , 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
相关文章