.. 瞧瞧别人家的异常处理,那叫一个优雅(苏三说技术2024年10月24日文章)_众股360

您的位置 : 首页 > 公众号 > 苏三说技术

瞧瞧别人家的异常处理,那叫一个优雅(苏三说技术2024年10月24日文章)

分享到:
作者:苏三说技术 | 更新时间:2024-10-28 09:52:20

大家好,我是苏三,又跟大家见面了。今天是1024程序员节,祝大家节日快乐。文末送了点福利,有需要的小伙伴可以领取一下。前言在我们日常工作中,经常会遇到一些异常,比如:NullPointerException、NumberFormatExce...

A股板块轮动加剧,跨年大妖来袭,这几只票主力已明显介入!微信搜索关注【研讯小组】公众号(可长按复制),回复666,领取代码!

大家好,我是苏三,又跟大家见面了。


今天是1024程序员节,祝大家节日快乐。


文末送了点福利,有需要的小伙伴可以领取一下。

前言

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

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

1 不要忽略异常

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

反例:

Longid=null;
try{
id=Long.parseLong(keyword);
}catch(NumberFormatExceptione){
//忽略异常
}

用户输入的参数,使用Long.parseLong方法转换成Long类型的过程中,如果出现了异常,则使用try/catch直接忽略了异常。

并且也没有打印任何日志。

如果后面线上代码出现了问题,有点不太好排查问题。

建议大家不要忽略异常,在后续的工作中,可能会带来很多麻烦。

正例:

Longid=null;
try{
id=Long.parseLong(keyword);
}catch(NumberFormatExceptione){
log.info(String.format("keyword:{}转换成Long类型失败,原因:{}",keyword,e))
}

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

2 使用全局异常处理器

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

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

反例:

try{
checkParam(param);
}catch(BusinessExceptione){
returnApiResultUtil.error(1,"参数错误");
}

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

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

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

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

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

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

例如:

@Slf4j
@RestControllerAdvice
publicclassGlobalExceptionHandler{

/**
*统一处理异常
*
*@parame异常
*@returnAPI请求响应实体
*/

@ExceptionHandler(Exception.class)
publicApiResulthandleException(Exceptione)
{
if(einstanceofBusinessException){
BusinessExceptionbusinessException=(BusinessException)e;
log.info("请求出现业务异常:",e);
returnApiResultUtil.error(businessException.getCode(),businessException.getMessage());
}
log.error("请求出现系统异常:",e);
returnApiResultUtil.error(HttpStatus.INTERNAL_SERVER_ERROR.value(),"服务器内部错误,请联系系统管理员!");
}

}

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

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

3 尽可能捕获具体异常

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

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

反例:

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

这样捕获异常太笼统了。

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

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

正例:

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

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

4 在finally中关闭IO流

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

我们需要在try/catch中处理IO流,因为可能会出现IO异常。

反例:

try{
Filefile=newFile("/tmp/1.txt");
FileInputStreamfis=newFileInputStream(file);
byte[]data=newbyte[(int)file.length()];
fis.read(data);
for(byteb:data){
System.out.println(b);
}
fis.close();
}catch(IOExceptione){
log.error("读取文件失败,原因:",e)
}

上面的代码直接在try的代码块中关闭fis。

假如在调用fis.read方法时,出现了IO异常,则可能会直接抛异常,进入catch代码块中,而此时fis.close方法没办法执行,也就是说这种情况下,无法正确关闭IO流。

正例:

FileInputStreamfis=null;
try{
Filefile=newFile("/tmp/1.txt");
fis=newFileInputStream(file);
byte[]data=newbyte[(int)file.length()];
fis.read(data);
for(byteb:data){
System.out.println(b);
}
}catch(IOExceptione){
log.error("读取文件失败,原因:",e)
}finally{
if(fis!=null){
try{
fis.close();
fis=null;
}catch(IOExceptione){
log.error("读取文件后关闭IO流失败,原因:",e)
}
}
}

在finally代码块中关闭IO流。

但要先判断fis不为空,否则在执行fis.close()方法时,可能会出现NullPointerException异常。

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

5 多用try-catch-resource

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

因此在JDK7之后,出现了一种新的语法糖try-with-resource。

上面的代码可以改造成这样的:

Filefile=newFile("/tmp/1.txt");
try(FileInputStreamfis=newFileInputStream(file)){
byte[]data=newbyte[(int)file.length()];
fis.read(data);
for(byteb:data){
System.out.println(b);
}
}catch(IOExceptione){
e.printStackTrace();
log.error("读取文件失败,原因:",e)
}

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

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

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

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

6 不在finally中return

我们在某个方法中,可能会有返回数据。

反例:

publicintdivide(intdividend,intdivisor){
try{
returndividend/divisor;
}catch(ArithmeticExceptione){
//异常处理
}finally{
return-1;
}
}

上面的这个例子中,我们在finally代码块中返回了数据-1。

这样最后在divide方法返回时,会将dividend / divisor的值覆盖成-1,导致正常的结果也不对。

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

正解:

publicintdivide(intdividend,intdivisor){
try{
returndividend/divisor;
}catch(ArithmeticExceptione){
//异常处理
return-1;
}
}

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

7 少用e.printStackTrace()

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

反例:

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

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

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

  1. 可能会暴露敏感信息,如文件路径、用户名、密码等。

  2. 可能会影响程序的性能和稳定性。

正解:

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

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

8 异常打印详细一点

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

反例:

try{
doubleb=1/0;
}catch(ArithmeticExceptione){
log.error("处理失败,原因:",e.getMessage());
}

这个例子中使用e.getMessage()方法返回异常信息。

但执行结果为:

doSomething处理失败,原因:

这种情况异常信息根本没有打印出来。

我们应该把异常信息和堆栈都打印出来。

正例:

try{
doubleb=1/0;
}catch(ArithmeticExceptione){
log.error("处理失败,原因:",e);
}

执行结果:

doSomething处理失败,原因:
java.lang.ArithmeticException:/byzero
atcn.net.susan.service.Test.main(Test.java:16)

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

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

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

反例:

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

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

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

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

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

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

10 优先使用标准异常

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

反例:

publicvoidcheckValue(intvalue){
if(value<0){
thrownewMyIllegalArgumentException("值不能为负");
}
}

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

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

正例:

publicvoidcheckValue(intvalue){
if(value<0){
thrownewIllegalArgumentException("值不能为负");
}
}

11 对异常进行文档说明

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

反例:

/*
*处理用户数据
*@paramvalue用户输入参数
*@return值
*/

publicintdoSomething(Stringvalue)
throwsBusinessException
{
//业务逻辑
return1;
}

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

正解:

/*
*处理用户数据
*@paramvalue用户输入参数
*@return值
*@throwsBusinessException业务异常
*/

publicintdoSomething(Stringvalue)
throwsBusinessException
{
//业务逻辑
return1;
}

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

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

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

反例:

Longid=null;
try{
id=Long.parseLong(idStr);
}catch(NumberFormatExceptione){
id=1001;
}

如果用户输入的idStr是Long类型,则将它转换成Long,然后赋值给id,否则id给默认值1001。

每次都需要try/catch还是比较影响系统性能的。

正例:

Longid=checkValueType(idStr)?Long.parseLong(idStr):1001;

我们增加了一个checkValueType方法,判断idStr的值,如果是Long类型,则直接转换成Long,否则给默认值1001。

13 自定义异常

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

例如:

/**
*业务异常
*
*@author苏三
*@date2024/1/9下午1:12
*/

@AllArgsConstructor
@Data
publicclassBusinessExceptionextendsRuntimeException{

publicstaticfinallongserialVersionUID=-6735897190745766939L;

/**
*异常码
*/

privateintcode;

/**
*具体异常信息
*/

privateStringmessage;

publicBusinessException(){
super();
}

publicBusinessException(Stringmessage){
this.code=HttpStatus.INTERNAL_SERVER_ERROR.value();
this.message=message;
}
}

对于这种自定义的业务异常,我们可以增加code和message这两个字段,code表示异常码,而message表示具体的异常信息。

BusinessException继承了RuntimeException运行时异常,后面处理起来更加灵活。

提供了多种构造方法。

定义了一个序列化ID(serialVersionUID)。


今天特地申请了10张50元的知识星球优惠券(最近半年最大优惠),有需求的小伙伴可以扫描下方二维码领取一下。

最后欢迎加入苏三的星球,你将获得:商城系统实战、秒杀系统实战、代码生成工具、系统设计、性能优化、技术选型、高频面试题、底层原理、Spring源码解读、工作经验分享、痛点问题等多个优质专栏。

  • 我的技术成长之路

  • 我的三个项目

  • 被官方推荐了

还有1V1答疑、修改简历、职业规划、送书活动、技术交流。

目前星球已经更新了4500+篇优质内容,还在持续爆肝中..星球已经被官方推荐了3次,收到了小伙伴们的一致好评。戳我加入学习,已有1400+小伙伴加入学习。


A股板块轮动加剧,跨年大妖来袭,这几只票主力已明显介入!微信搜索关注【研讯小组】公众号(可长按复制),回复666,领取代码!

本站内容转载请注明来源并提供链接,数据来自互联网,仅供参考。如发现侵权行为,请联系我们删除涉嫌侵权内容。

展开

相关文章

更多>>

反馈与咨询

关于本站 反馈中心 版权声明 网站地图

  版权投诉请发邮件到1191009458#qq.com(把#改成@),我们会尽快处理

  Copyright©2023-2024众股360(www.zgu360.com).AllReserved|备案号:湘ICP备2023009521号-3

  本站资源均收集整理于互联网,其著作权归原作者所有,如有侵犯你的版权,请来信告知,我们将及时下架删除相应资源

Copyright © 2024-2024 EYOUCMS. 易优CMS 版权所有 Powered by EyouCms