如何优雅处理异常?处理异常的原则

前言

在我们日常工作中,经常会遇到一些异常,比如: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 rice
 * @date 2024/11/5
 */
@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)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/912992.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

海量数据迁移:Elasticsearch到OpenSearch的无缝迁移策略与实践

文章目录 一&#xff0e;迁移背景二&#xff0e;迁移分析三&#xff0e;方案制定3.1 使用工具迁移3.2 脚本迁移 四&#xff0e;方案建议 一&#xff0e;迁移背景 目前有两个es集群&#xff0c;版本为5.2.2和7.16.0&#xff0c;总数据量为700T。迁移过程需要不停服务迁移&#…

macOS开发环境配置与应用开发(详细讲解)

&#x1f4dd;个人主页&#x1f339;&#xff1a;一ge科研小菜鸡-CSDN博客 &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; 1. 引言 macOS作为Apple公司推出的桌面操作系统&#xff0c;以其稳定性、优雅的用户界面和强大的开发工具吸引了大量开发者。对于…

【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法

【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法 【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和深度迁移学习的遥感影像滑坡制图方法 文章目录 【深度学习滑坡制图|论文解读3】基于融合CNN-Transformer网络和…

前端学习之ES6+

1.ES6是什么 ES6&#xff0c;全称是ECMAScript 6&#xff0c;是JavaScript语言的下一代标准&#xff0c;由ECMA国际组织在2015年6月正式发布。ES6也被称作ECMAScript 2015&#xff0c;从这个版本开始&#xff0c;ECMA组织决定每年发布一个新的ECMAScript版本&#xff0c;以使J…

查缺补漏----用户上网过程(HTTP,DNS与ARP)

&#xff08;1&#xff09;HTTP 来自湖科大计算机网络微课堂&#xff1a; ① HTTP/1.0采用非持续连接方式。在该方式下&#xff0c;每次浏览器要请求一个文件都要与服务器建立TCP连接当收到响应后就立即关闭连接。 每请求一个文档就要有两倍的RTT的开销。若一个网页上有很多引…

【广西】《广西壮族自治区本级政务信息化建设和运维项目预算支出标准》(桂财建〔2023〕102号)-省市费用标准解读系列09

《广西壮族自治区本级政务信息化建设和运维项目预算支出标准》&#xff08;桂财建〔2023〕102号&#xff09;是广西壮族自治区财政厅于2023年9月26日发布的费用标准&#xff08;了解更多可直接关注我们咨询&#xff09;。我司基于专业第三方信息化项目造价机构角度&#xff0c;…

Linux基础-常用操作命令详讲

Linux基础-常用操作命令详讲 一、openssl加密简单介绍 1. 生成加密的密码散列&#xff08;password hash&#xff09;​编辑 1.1 常见的选项总结表 1.2 加密参数详解 2. 自签名证书 3. 证书转换 二、文件管理 1. 创建空文件 ​编辑 2. 删除文件 4. 新建目录 ​编辑…

ALB搭建

ALB: 多级分发、消除单点故障提升应用系统的可用性&#xff08;健康检查&#xff09;。 海量微服务间的高效API通信。 自带DDoS防护&#xff0c;集成Web应用防火墙 配置&#xff1a; 1.创建ECS实例 2.搭建应用 此处安装的LNMP 3.创建应用型负载均衡ALB实例 需要创建服务关联角…

C语言笔记(字符串函数,字符函数,内存函数)

目录 前言 1.字符串函数 1.1.strlen 1.2.strcpy 1.3.strcat 1.4.strcmp 1.5.strncpy 1.6.strncat 1.7.strncmp 1.8.strstr 1.9.strtok 1.10.strerror 2.字符函数 2.1字符分类函数 2.2字符转换函数 3.内存函数 3.1.mencpy 3.2.memmove 3.3.memcmp 前言 本文重…

HCIP-HarmonyOS Application Developer V1.0 笔记(五)

弹窗功能 prompt模块来调用系统弹窗API进行弹窗制作。 当前支持3种弹窗API&#xff0c;分别为&#xff1a; 文本弹窗&#xff0c;prompt.showToast&#xff1b;对话框&#xff0c;prompt.showDialog&#xff1b;操作菜单&#xff0c;prompt.showActionMenu。 要使用弹窗功能&…

Linux相关概念和易错知识点(20)(dentry、分区、挂载)

目录 1.dentry &#xff08;1&#xff09;路径缓存的原因 &#xff08;2&#xff09;dentry的结构 ①多叉树结构 ②file和dentry之间的联系 ③路径概念存在的意义 2.分区 &#xff08;1&#xff09;为什么要确认分区 &#xff08;2&#xff09;挂载 ①进入分区 ②被挂…

Redis 缓存击穿

目录 缓存击穿 什么是缓存击穿&#xff1f; 有哪些解决办法&#xff1f; 缓存穿透和缓存击穿有什么区别&#xff1f; 缓存雪崩 什么是缓存雪崩&#xff1f; 有哪些解决办法&#xff1f; 缓存预热如何实现&#xff1f; 缓存雪崩和缓存击穿有什么区别&#xff1f; 如何保…

电信网关配置管理系统 upload_channels.php 文件上传致RCE漏洞复现

0x01 产品简介 中国电信集团有限公司(英文名称“China Telecom”、简称“中国电信”)成立于2000年9月,是中国特大型国有通信企业、上海世博会全球合作伙伴。电信网关配置管理系统是一个用于管理和配置电信网络中网关设备的软件系统。它可以帮助网络管理员实现对网关设备的远…

澳鹏通过高质量数据支持 Onfido 优化AI反欺诈功能

“Appen 在 Onfido 的发展中发挥了至关重要的作用&#xff0c;并已成为我们运营的重要组成部分。我们很高兴在 Appen 找到了可靠的合作伙伴。” – Onfido 数据和分析总监 Francois Jehl 简介&#xff1a;利用人工智能和机器学习增强欺诈检测 在当今日益数字化的世界&#xff…

网站架构知识之Ansible模块(day021)

1.Ansible模块 作用:通过ansible模块实现批量管理 2.command模块与shell模块 command模块是ansible默认的模块&#xff0c;适用于执行简单的命令&#xff0c;不支持特殊符号 案列01&#xff0c;批量获取主机名 ansible all -m command -a hostname all表示对主机清单所有组…

应对AI与机器学习的安全与授权管理新挑战,CodeMeter不断创新引领保护方案

人工智能&#xff08;AI&#xff09;和机器学习&#xff08;ML&#xff09;技术正在快速发展&#xff0c;逐渐应用到全球各类主流系统、设备及关键应用场景中&#xff0c;尤其是在政府、商业和工业组织不断加深互联的情况下&#xff0c;AI和ML技术的影响日益广泛。虽然AI技术的…

实现uniapp-微信小程序 搜索框+上拉加载+下拉刷新

pages.json 中的配置 { "path": "pages/message", "style": { "navigationBarTitleText": "消息", "enablePullDownRefresh": true, "onReachBottomDistance": 50 } }, <template><view class…

布谷直播源码部署服务器关于数据库配置的详细说明

布谷直播源码搭建部署配置接口数据库 /public/db.php&#xff08;2019年8月后的系统在该路径下配置数据库&#xff0c;老版本继续走下面的操作&#xff09; 在项目代码中执行命令安装依赖库&#xff08;⚠️注意&#xff1a;如果已经有了vendor内的依赖文件的就不用执行了&am…

【C++】STL— stack的常见用法和模拟实现

目录 1、stack的介绍 2、stack的使用 构造一个空栈 stack的简单接口应用 3、stack的模拟实现 4、栈的相关题目 4.1 最小栈 4.1.2思路 4.1.3 实现代码 4.2 栈的压入、弹出序列 4.2.2 思路 4.2.3程序实现 1、stack的介绍 在C中&#xff0c;stack是一种标准模板库&am…

vue大疆建图航拍功能实现

介绍 无人机在规划一块区域的时候&#xff0c;我们需要手动的给予一些参数来影响无人机飞行&#xff0c;对于一块地表&#xff0c;无人机每隔N秒在空中间隔的拍照地表的一块区域&#xff0c;在整个任务执行结束后&#xff0c;拍到的所有区域照片能够完整的表达出一块地表&…