⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 苏三说技术 「苏三」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

在我们日常工作中,经常会遇到一些异常,比如:NullPointerException、NumberFormatException、ClassCastException等等。

那么问题来了,我们该如何处理异常,让代码变得更优雅呢?

1 不要忽略异常

不知道你有没有遇到过下面这段代码:

反例:

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))
}

后面如果数据转换出现问题,从日志中我们一眼就可以查到具体原因了。

2 使用全局异常处理器

有些小伙伴,经常喜欢在Service代码中捕获异常。

不管是普通异常Exception,还是运行时异常RuntimeException,都使用try/catch把它们捕获。

反例:

try {
checkParam(param);
} catch (BusinessException e) {
return ApiResultUtil.error(1,"参数错误");
}

在每个Controller类中都捕获异常。

在UserController、MenuController、RoleController、JobController等等,都有上面的这段代码。

显然这种做法会造成大量重复的代码。

我们在Controller、Service等业务代码中,尽可能少捕获异常。

这种业务异常处理,应该交给拦截器统一处理。

在SpringBoot中可以使用@RestControllerAdvice注解,定义一个全局的异常处理handler,然后使用@ExceptionHandler注解在方法上处理异常。

例如:

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

/**
* 统一处理异常
*
* @param e 异常
* @return API请求响应实体
*/
@ExceptionHandler(Exception.class)
public ApiResult handleException(Exception e) {
if (e instanceof BusinessException) {
BusinessException businessException = (BusinessException) e;
log.info("请求出现业务异常:", e);
return ApiResultUtil.error(businessException.getCode(), businessException.getMessage());
}
log.error("请求出现系统异常:", e);
return ApiResultUtil.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "服务器内部错误,请联系系统管理员!");
}

}

有了这个全局的异常处理器,之前我们在Controller或者Service中的try/catch代码可以去掉。

如果在接口中出现异常,全局的异常处理器会帮我们封装结果,返回给用户。

3 尽可能捕获具体异常

在你的业务逻辑方法中,有可能需要去处理多种不同的异常。

你可能你会觉得比较麻烦,而直接捕获Exception。

反例:

try {
doSomething();
} catch(Exception e) {
log.error("doSomething处理失败,原因:",e);
}

这样捕获异常太笼统了。

其实doSomething方法中,会抛出FileNotFoundException和IOException。

这种情况我们最好捕获具体的异常,然后分别做处理。

正例:

try {
doSomething();
} catch(FileNotFoundException e) {
log.error("doSomething处理失败,文件找不到,原因:",e);
} catch(IOException e) {
log.error("doSomething处理失败,IO出现了异常,原因:",e);
}

这样如果后面出现了上面的异常,我们就非常方便知道是什么原因了。

4 在finally中关闭IO流

我们在使用IO流的时候,用完了之后,一般需要及时关闭,否则会浪费系统资源。

我们需要在try/catch中处理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异常,则可能会直接抛异常,进入catch代码块中,而此时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异常。

需要注意的地方时,在调用fis.close()方法时,也可能会抛异常,我们还需要进行try/catch处理。

5 多用try-catch-resource

前面在finally代码块中关闭IO流,还是觉得有点麻烦。

因此在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括号里头的FileInputStream实现了一个AutoCloseable接口,所以无论这段代码是正常执行完,还是有异常往外抛,还是内部代码块发生异常被截获,最终都会自动关闭IO流。

我们尽量多用try-catch-resource的语法关闭IO流,可以少写一些finally中的代码。

而且在finally代码块中关闭IO流,有顺序的问题,如果有多种IO,关闭的顺序不对,可能会导致部分IO关闭失败。

而try-catch-resource就没有这个问题。

6 不在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,导致正常的结果也不对。

我们尽量不要在finally代码块中返回数据。

正解:

public int divide(int dividend, int divisor) {
try {
return dividend / divisor;
} catch (ArithmeticException e) {
// 异常处理
return -1;
}
}

如果dividend / divisor出现了异常,则在catch代码块中返回-1。

7 少用e.printStackTrace()

我们在本地开发中,喜欢使用e.printStackTrace()方法,将异常的堆栈跟踪信息输出到标准错误流中。

反例:

try {
doSomething();
} catch(IOException e) {
e.printStackTrace();
}

这种方式在本地确实容易定位问题。

但如果代码部署到了生产环境,可能会带来下面的问题:

  1. 可能会暴露敏感信息,如文件路径、用户名、密码等。
  2. 可能会影响程序的性能和稳定性。

正解:

try {
doSomething();
} catch(IOException e) {
log.error("doSomething处理失败,原因:",e);
}

我们要将异常信息记录到日志中,而不是保留给用户。

8 异常打印详细一点

我们在捕获了异常之后,需要把异常的相关信息记录到日志当中。

反例:

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)

将具体的异常,出现问题的代码和具体行数都打印出来。

9 别捕获了异常又马上抛出

有时候,我们为了记录日志,可能会对异常进行捕获,然后又抛出。

反例:

try {
doSomething();
} catch(ArithmeticException e) {
log.error("doSomething处理失败,原因:",e)
throw e;
}

在调用doSomething方法时,如果出现了ArithmeticException异常,则先使用catch捕获,记录到日志中,然后使用throw关键抛出这个异常。

这个骚操作纯属是为了记录日志。

但最后发现日志记录两次。

因为在后续的处理中,可能会将这个ArithmeticException异常又记录一次。

这样就会导致日志重复记录了。

10 优先使用标准异常

在Java中已经定义了许多比较常用的标准异常,比如下面这张图中列出的这些异常:

反例:

public void checkValue(int value) {
if (value < 0) {
throw new MyIllegalArgumentException("值不能为负");
}
}

自定义了一个异常表示参数错误。

其实,我们可以直接复用已有的标准异常。

正例:

public void checkValue(int value) {
if (value < 0) {
throw new IllegalArgumentException("值不能为负");
}
}

11 对异常进行文档说明

我们在写代码的过程中,有一个好习惯是给方法、参数和返回值,增加文档说明。

反例:

/*  
* 处理用户数据
* @param value 用户输入参数
* @return 值
*/
public int doSomething(String value)
throws BusinessException {
//业务逻辑
return 1;
}

这个doSomething方法,把方法、参数、返回值都加了文档说明,但异常没有加。

正解:

/*  
* 处理用户数据
* @param value 用户输入参数
* @return 值
* @throws BusinessException 业务异常
*/
public int doSomething(String value)
throws BusinessException {
//业务逻辑
return 1;
}

抛出的异常,也需要增加文档说明。

12 别用异常控制程序的流程

我们有时候,在程序中使用异常来控制了程序的流程,这种做法其实是不对的。

反例:

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。

13 自定义异常

如果标准异常无法满足我们的业务需求,我们可以自定义异常。

例如:

/**
* 业务异常
*
* @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)。

文章目录
  1. 1. 1 不要忽略异常
  2. 2. 2 使用全局异常处理器
  3. 3. 3 尽可能捕获具体异常
  4. 4. 4 在finally中关闭IO流
  5. 5. 5 多用try-catch-resource
  6. 6. 6 不在finally中return
  7. 7. 7 少用e.printStackTrace()
  8. 8. 8 异常打印详细一点
  9. 9. 9 别捕获了异常又马上抛出
  10. 10. 10 优先使用标准异常
  11. 11. 11 对异常进行文档说明
  12. 12. 12 别用异常控制程序的流程
  13. 13. 13 自定义异常