阿里妹导读 如何处理Java异常?作者查看了一些异常处理的规范,对 Java 异常处理机制有更深入的了解,并将自己的学习内容记录下来,希望对有同样困惑的同学提供一些帮助。 一、概述
二、java 异常处理机制
1、java 异常分类
说明:检查异常和非检查异常是针对编译器而言的,是编译器来检查该异常是否强制开发人员处理该异常:
建议使用非检查异常让代码更加简洁,而且更容易保持接口的稳定性。
2、从字节码层面分析异常处理
异常表:异常表中用来记录程序计数器的位置和异常类型。如上图所示,表示的意思是:如果在 8 到 16 (不包括16)之间的指令抛出的异常匹配 MyCheckedException 类型的异常,那么程序跳转到16 的位置继续执行。
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() 方法的字节码:
/*** 打包多个文件为 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 块中手动关闭资源。
三、java 异常处理不规范案例
捕获
try{……} catch (Exception e){ // 不应对所有类型的异常统一捕获,应该抽象出业务异常和系统异常,分别捕获……}
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异常处理规约
后面的章节我将根据自己的思考,说明如何定义异常,如何抛出异常,如何处理异常,接着往下看。 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();}
/*** 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类的任意方法的执行
基于请求转发的方式:真正的全局异常处理。
基于异常处理器的方式:不是真正的全局异常处理,因为它处理不了过滤器等抛出的异常。
基于过滤器的方式:近似全局异常处理。它能处理过滤器及之后的环节抛出的异常。
<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
微信扫一扫
关注该公众号