概述
Aliware
了解Java异常的分类,什么是检查异常,什么是非检查异常 从字节码层面理解Java的异常处理机制,为什么finally块中的代码总是会执行 了解Java异常处理的不规范案例 了解Java异常处理的最佳实践 了解项目中的异常处理,什么时候抛出异常,什么时候捕获异常
Java 异常处理机制
Aliware
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 异常处理不规范案例
Aliware
捕获
捕获异常的时候不区分异常类型 捕获异常不完全,比如该捕获的异常类型没有捕获到
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 异常处理规范案例
Aliware
1、阿里巴巴Java异常处理规约
【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。 【推荐】定义时区分unchecked / checked 异常,避免直接使用RuntimeException抛出,更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。
后面的章节我将根据自己的思考,说明如何定义异常,如何抛出异常,如何处理异常,接着往下看。
2、异常处理最佳实践
logger.error("说明信息,异常信息:{}", e.getMessage(), e)throw MyException("my exception", e);9、自定义异常尽量不要使用检查异常。
项目中的异常处理实践
Aliware
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 类**/@Slf4j@Aspect@Componentpublic class RpcGlobalExceptionAop {/*** execution(* com.xyz.service ..*.*(..)):表示 rpc 接口实现类包中的所有方法*/@Pointcut("execution(* com.xyz.service ..*.*(..))")public void pointcut() {}@Around(value = "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 接口异常处理类*/@Slf4j@RestControllerAdvice("org.example.controller")public class HttpExceptionHandler {/*** 处理业务异常* @param request 请求参数* @param e 异常* @return Result*/@ExceptionHandler(value = BizException.class)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*/@ExceptionHandler(value = SystemException.class)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*/@ExceptionHandler(value = Throwable.class)public Object unknownExceptionHandler(HttpServletRequest request, Throwable e) {log.error("未知异常:" + e.getMessage() , e);return Result.fail(e.getMessage());}}
在 HttpExceptionHandler 类中,@RestControllerAdvice = @ControllerAdvice + @ResponseBody ,如果有其他的异常需要处理,只需要定义@ExceptionHandler注解的方法处理即可。
总结
Aliware
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