java常见异常的继承体系:
在实际开发工作中, 异常是百分之百会遇到的,也是必须要处理的,所以 对于异常的处理,一定要妥善而行。
可能的异常及时处理
反例:
Long id = null;
try {
id = Long.parseLong(keyword);
} catch(NumberFormatException e) {
//忽略异常
}
用户输入的参数,使用Long.parseLong方法转换成Long类型的过程中,如果出现了异常,则使用try/catch直接忽略了异常。
并且也没有打印任何日志。
如果后面线上代码出现了问题,有点不太好排查问题。
建议大家 预判可能出现的异常及时处理 。
正例:
Long id = null;
try {
id = Long.parseLong(keyword);
} catch(NumberFormatException e) {
log.info(String.format("keyword:{} 转换成Long类型失败,原因:{}",keyword , e))
}
后面如果数据转换出现问题,从日志中我们一眼就可以查到具体原因了。
使用全局异常处理器
有些同学,经常在Service代码中捕获异常。
不管是普通异常Exception,还是运行时异常RuntimeException,都使用try/catch把它们捕获。
反例:
try {
checkParam(param);
} catch (BusinessException e) {
return ApiResultUtil.error(1,"参数错误");
}
在Controller、Service等业务代码中,尽可能少捕获异常。
这种业务异常处理,应该交给 拦截器 统一处理。
在SpringBoot中可以使用 @ControllerAdvice注解 来创建全局异常处理器,它可以处理控制器抛出的所有异常。
@ControllerAdvice:该注解是 spring2.3 以后新增的一个注解。
@ExceptionHandler:该注解是配合@ControllerAdvice 一起使用的注解,可以自定义错误处理器,自 行组装 json 字符串,并返回到页面。
例如:
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorResponseData<?> runTimeError(Throwable e) {
log.error("服务器运行异常", e);
return new ErrorResponseData<>("500",e.getMessage());
}
@ExceptionHandler(IOException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorResponseData<?> ioError(Throwable e) {
log.error("io运行异常", e);
return new ErrorResponseData<>("500",e.getMessage());
}
@ExceptionHandler(IOException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public ErrorResponseData<?> sqlError(Throwable e) {
log.error("sql运行异常", e);
return new ErrorResponseData<>("500",e.getMessage());
}
}
有了这个全局的异常处理器,之前我们在Controller或者Service中的try/catch代码可以去掉。
如果在接口中出现异常,全局的异常处理器会帮我们封装结果,返回给用户。
异常捕获要具体化
不建议直接笼统地捕获Exception。
反例:
try {
wtt();
} catch(Exception e) {
log.error("doSomething处理失败,原因:",e);
}
假设 wtt方法中,会抛出FileNotFoundException 和 IOException。
这种情况我们最好捕获具体的异常,然后分别做处理。
正例:
try {
wtt();
} catch(FileNotFoundException e) {
log.error("文件找不到,原因:",e);
} catch(IOException e) {
log.error("IO出现了异常,原因:",e);
}
这样如果后面出现了上面的异常,就非常方便知道是什么原因了。
在finally中关闭 IO资源流
IO流用完了之后,一般需要及时关闭。
反例:
try {
File file = new File("/tmp/1.txt");
FileInputStream fis = new FileInputStream(file);
byte[] data = new byte[(int) file.length()];
fis.read(data);
for (byte b : data) {
System.out.println(b);
}
fis.close();
} catch (IOException e) {
log.error("读取文件失败,原因:",e)
}
上面的代码直接在 try的代码块 中关闭fis。
假如在调用fis.read方法时,出现了IO异常,则可能会直接抛异常,此时fis.close方法没办法执行,也就是说这种情况下,无法正确关闭IO流。
正例:
FileInputStream fis = null;
try {
File file = new File("/tmp/1.txt");
fis = new FileInputStream(file);
byte[] data = new byte[(int) file.length()];
fis.read(data);
for (byte b : data) {
System.out.println(b);
}
} catch (IOException e) {
log.error("读取文件失败,原因:",e)
} finally {
if(fis != null) {
try {
fis.close();
fis = null;
} catch (IOException e) {
log.error("读取文件后关闭IO流失败,原因:",e)
}
}
}
在 finally代码块 中关闭IO流。
但要先判断 fis不为空,否则在执行fis.close()方法时,可能会出现NullPointerException异常。
语法糖: try-catch-resource
在JDK7之后,出现了一种新的语法糖try-with-resource。
上面的代码可以改造成这样的:
File file = new File("/tmp/1.txt");
try (FileInputStream fis = new FileInputStream(file)) {
byte[] data = new byte[(int) file.length()];
fis.read(data);
for (byte b : data) {
System.out.println(b);
}
} catch (IOException e) {
e.printStackTrace();
log.error("读取文件失败,原因:",e)
}
我们尽量多用try-catch-resource的语法关闭IO流,可以少写一些finally中的代码。
而且在finally代码块中关闭IO流,有顺序的问题,如果有多种IO,关闭的顺序不对,可能会导致部分IO关闭失败。
而try-catch-resource就没有这个问题。
不要 finally中进行 return 行为
在某个方法中,可能会有返回数据。
反例:
public int divide(int dividend, int divisor) {
try {
return dividend + divisor;
} catch (ArithmeticException e) {
// 异常处理
} finally {
return -1;
}
}
上面的这个例子中,我们在finally代码块中返回了数据-1。
这样最后在divide方法返回时,会将dividend + divisor的值 覆盖成-1,导致正常的结果也不对。
可以在 catch 中 返回:
public int divide(int dividend, int divisor) {
try {
return dividend + divisor;
} catch (ArithmeticException e) {
// 异常处理
return -1;
}
}
生产环境中 远离 e.printStackTrace()
本地开发中,经常使用e.printStackTrace()方法,将异常的堆栈跟踪信息输出到标准错误流中。
反例:
try {
doSomething();
} catch(IOException e) {
e.printStackTrace();
}
这种方式在本地确实容易定位问题。
但如果代码部署到了生产环境,可能会带来下面的问题:
- 可能会暴露敏感信息,如文件路径、用户名、密码等。
- 可能会影响程序的性能和稳定性。
正解:
try {
doSomething();
} catch(IOException e) {
log.error("doSomething处理失败,原因:",e);
}
我们要将异常信息记录到日志中,而不是保留给用户。
完整描述 异常
在捕获了异常之后,需要把异常的相关信息记录到日志当中。
反例:
try {
double b = 1/0;
} catch(ArithmeticException e) {
log.error("处理失败,原因:",e.getMessage());
}
这个例子中使用e.getMessage()方法返回异常信息。
但执行结果为:
doSomething处理失败,原因:
这种情况异常信息根本没有打印出来。
我们应该把异常信息和堆栈都打印出来。
正例:
try {
double b = 1/0;
} catch(ArithmeticException e) {
log.error("处理失败,原因:",e);
}
执行结果:
doSomething处理失败,原因:
java.lang.ArithmeticException: / by zero
at cn.net.susan.service.Test.main(Test.java:16)
将具体的异常,出现问题的代码和具体行数都打印出来。
对 异常 进行文档说明
给方法、参数和返回值,增加文档说明 是一个好习惯。
反例:
/*
* 处理用户数据
* @param value 用户输入参数
* @return 值
*/
public int doSomething(String value)
throws BusinessException {
//业务逻辑
return 1;
}
但 异常 没有加 文档说明。
正解:
/*
* 处理用户数据
* @param value 用户输入参数
* @return 值
* @throws BusinessException 业务异常
*/
public int doSomething(String value)
throws BusinessException {
//业务逻辑
return 1;
}
抛出的异常,也需要增加文档说明。
捕获了异常 不要 马上抛出
有时候,我们为了记录日志,可能会对异常进行捕获,然后又抛出。
反例:
try {
doSomething();
} catch(ArithmeticException e) {
log.error("doSomething处理失败,原因:",e)
throw e;
}
这样就会导致日志重复记录了。
异常捕获 不要 参与到 正常的程序流程
在程序中使用异常来控制了程序的流程,这种做法看似很酷,其实是不对的。
反例:
Long id = null;
try {
id = Long.parseLong(idStr);
} catch(NumberFormatException e) {
id = 1001;
}
如果用户输入的idStr是Long类型,则将它转换成Long,然后赋值给id,否则id给默认值1001。
每次都需要try/catch还是比较影响系统性能的。
正例:
Long id = checkValueType(idStr) ? Long.parseLong(idStr) : 1001;
我们增加了一个checkValueType方法,判断idStr的值,如果是Long类型,则直接转换成Long,否则给默认值1001。
自定义合理的异常
如果标准异常无法满足我们的业务需求,我们可以自定义异常。
例如:
/**
* 业务异常
*
* @author 苏三
* @date 2024/1/9 下午1:12
*/
@AllArgsConstructor
@Data
public class BusinessException extends RuntimeException {
public static final long serialVersionUID = -6735897190745766939L;
/**
* 异常码
*/
private int code;
/**
* 具体异常信息
*/
private String message;
public BusinessException() {
super();
}
public BusinessException(String message) {
this.code = HttpStatus.INTERNAL_SERVER_ERROR.value();
this.message = message;
}
}
对于这种自定义的业务异常,我们可以增加 code 和 message 这两个字段,
code表示异常码,而message表示具体的异常信息。
BusinessException继承了RuntimeException运行时异常,后面处理起来更加灵活。
提供了多种构造方法。
定义了一个序列化ID(serialVersionUID)。