阿里妹导读
如何处理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