《开发实战》12 | 异常处理:别让自己在出问题的时候变为瞎子

news2025/1/10 10:14:29

12 | 异常处理:别让自己在出问题的时候变为瞎子

捕获和处理异常容易犯的错

“统一异常处理”方式正是我要说的第一个错:不在业务代码层面考虑异常处理,仅在框架层面粗犷捕获和处理异常
为了理解错在何处,先看看大多数业务应用都采用的三层架构:

  1. Controller 层负责信息收集、参数校验、转换服务层处理的数据适配前端,轻业务逻辑;
  2. Service 层负责核心业务逻辑,包括各种外部服务调用、访问数据库、缓存处理、消息处理等;
  3. Repository 层负责数据访问实现,一般没有业务逻辑。

在这里插入图片描述

@RestControllerAdvice
@Slf4j
public class RestControllerExceptionHandler {
  private static int GENERIC_SERVER_ERROR_CODE = 2000;
  private static String GENERIC_SERVER_ERROR_MESSAGE = "服务器忙,请稍后再试";
  @ExceptionHandler
  public APIResponse handle(HttpServletRequest req, HandlerMethod method, Exception BusinessException){
    if (ex instanceof BusinessException) {
      BusinessException exception = (BusinessException) ex;
      log.warn(String.format("访问 %s -> %s 出现业务异常!", req.getRequestU));
      return new APIResponse(false, null, exception.getCode(), exception);
    } else {
      log.error(String.format("访问 %s -> %s 出现系统异常!", req.getRequest
      return new APIResponse(false, null, GENERIC_SERVER_ERROR_CODE, GENE
    }
  }
}

出现运行时系统异常后,异常处理程序会直接把异常转换为 JSON 返回给调用方
捕获了异常后直接生吞。在任何时候,我们捕获了异常都不应该生吞,也就是直接丢弃异常不记录、不抛出
丢弃异常的原始信息。我们来看两个不太合适的异常处理方式,虽然没有完全生吞异常,但也丢失了宝贵的异常信息。
抛异常

只记录了异常消息,却丢失了异常的类型、栈等重要信息:
catch (IOException e) {
  //原始异常信息丢失
  throw new RuntimeException("系统忙请稍后再试");
}

只知道文件读取错误的文件名,至于为什么读取错误、是不存在还是没权限,完全不知道。
catch (IOException e) {
  //只保留了异常消息,栈没有记录
  log.error("文件读取错误, {}", e.getMessage());
  throw new RuntimeException("系统忙请稍后再试");
}

抛出异常时不指定任何消息
throw new RuntimeException();

应该这样抛:

catch (IOException e) {
  log.error("文件读取错误", e);
  throw new RuntimeException("系统忙请稍后再试");
}
或者,把原始异常作为转换后新异常的 cause,原始异常信息同样不会丢:
catch (IOException e) {
  throw new RuntimeException("系统忙请稍后再试", e);
}

除了通过日志正确记录异常原始信息外,通常还有三种处理模式
转换,即转换新的异常抛出。对于新抛出的异常,最好具有特定的分类和明确的异常消息,而不是随便抛一个无关或没有任何信息的异常,并最好通过 cause 关联老异常。
重试,即重试之前的操作。比如远程调用服务端过载超时的情况,盲目重试会让问题更严重,需要考虑当前情况是否适合重试。
恢复,即尝试进行降级处理,或使用默认值来替代原始数据。

小心 finally 中的异常

finally也会抛异常

@GetMapping("wrong")
public void wrong() {
  try {
    log.info("try");
    //异常丢失
    throw new RuntimeException("try");
  } finally {
    log.info("finally");
    throw new RuntimeException("finally");
  }
}

最后在日志中只能看到 finally 中的异常,虽然 try 中的逻辑出现了异常,但却被 finally中的异常覆盖了
因为一个方法无法出现两个异常
修复方式:finally 代码块自己负责异常捕获和处理,或者可以把 try 中的异常作为主异常抛出,使用ddSuppressed 方法把 finally 中的异常附加到主异常上.

最好还是使用try-with-resources 语句的做法。对于实现了 AutoCloseable 接口的资源,建议使用 try-with-resources 来释放资源,否则也可能会产生刚才提到的,释放资源时出现的异常覆盖主异常的问题。比如如下我们定义一个测试资源,其 read 和 close 方法都会抛出异常:

public class TestResource implements AutoCloseable {
  public void read() throws Exception{
    throw new Exception("read error");
  }
  @Override
  public void close() throws Exception {
    throw new Exception("close error");
  }
}
@GetMapping("useresourcewrong")
public void useresourcewrong() throws Exception {
  TestResource testResource = new TestResource();
  try {
    testResource.read();
  } finally {
    testResource.close();
  }
}

而改为 try-with-resources 模式之后:

@GetMapping("useresourceright")
public void useresourceright() throws Exception {
  try (TestResource testResource = new TestResource()){
    testResource.read();
  }
}

千万别把异常定义为静态变量

public class Exceptions {
  public static BusinessException ORDEREXISTS = new BusinessException("订单已经存在");
  ...
}
@GetMapping("wrong")
public void wrong() {
  try {
    createOrderWrong();
  } catch (Exception ex) {
    log.error("createOrder got error", ex);
  }
  try {
    cancelOrderWrong();
  } catch (Exception ex) {
    log.error("cancelOrder got error", ex);
  }
}
private void createOrderWrong() {
  //这里有问题
  throw Exceptions.ORDEREXISTS;
}
private void cancelOrderWrong() {
  //这里有问题
  throw Exceptions.ORDEREXISTS;
}
cancelOrder got error 的提示对应了 createOrderWrong 方法。显然,cancelOrderWrong 方法在出错后抛出的异常,其实是 createOrderWrong 方法出错的异常

修复方式

public class Exceptions {
  public static BusinessException orderExists(){
    return new BusinessException("订单已经存在", 3001);
  }
}

提交线程池的任务出了异常会怎么样?

提交 10 个任务到线程池异步处理,第 5 个任务抛出一个RuntimeException,每个任务完成后都会输出一行日志:

@GetMapping("execute")
public void execute() throws InterruptedException {

    String prefix = "test";
    ExecutorService threadPool = Executors.newFixedThreadPool(1, new ThreadFactoryBuilder().setNameFormat(prefix+"%d").get());
    //提交10个任务到线程池处理,第5个任务会抛出运行时异常
    IntStream.rangeClosed(1, 10).forEach(i -> threadPool.execute(() -> {
        if (i == 5) throw new RuntimeException("error");
        log.info("I'm done : {}", i);
    }));

    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);
}

观察日志发现:任务 1 到 4 所在的线程是 test0,任务 6 开始运行在线程 test1。因此从线程名的改变可以知
道因为异常的抛出老线程退出了,线程池只能重新创建一个线程。如果每个异步任务都以异常结束,那么线程池可能完全起不到线程重用的作用。
因为没有手动捕获异常进行处理,ThreadGroup 帮我们进行了未捕获异常的默认处理,向标准错误输出打印了出现异常的线程名称和异常信息。显然,这种没有以统一的错误日志格式记录错误信息打印出来的形式,对生产级代码是不合适的
修复方式有 种

  1. 以 execute 方法提交到线程池的异步任务,最好在任务内部做好异常处理;设置自定义的异常处理程序作为保底,比如在声明线程池时自定义线程池的未捕获异常处理程序:
new ThreadFactoryBuilder()
  .setNameFormat(prefix+"%d")
  .setUncaughtExceptionHandler((thread, throwable)-> log.error("ThreadPool {} got exception", thread, throwable))
  .get()
  1. 设置全局的默认未捕获异常处理程序:
static {
    Thread.setDefaultUncaughtExceptionHandler((thread, throwable)->
     log.error("Thread {} got exception", thread, throwable));
}

把 execute 方法改为 submit,修改代码后重新执行程序可以看到如下日志,说明线程没退出,异常也没记录被生吞了.
查看 FutureTask 源码可以发现,在执行任务出现异常之后,异常存到了一个 outcome 字段中,只有在调用 get 方法获取 FutureTask 结果的时候,才会以 ExecutionException 的形式重新抛出异常:
修改后的代码如下所示,我们把 submit 返回的 Future 放到了 List 中,随后遍历 List 来捕获所有任务的异常。这么做确实合乎情理。既然是以 submit 方式来提交任务,那么我们应该关心任务的执行结果,否则应该以 xecute 来提交任务:

List<Future> tasks = IntStream.rangeClosed(1, 10).mapToObj(i -> threadPool.submit(() -> {
    if (i == 5) throw new RuntimeException("error");
    log.info("I'm done : {}", i);
})).collect(Collectors.toList());

tasks.forEach(task-> {
    try {
        task.get();
    } catch (Exception e) {
        log.error("Got exception", e);
    }
});

确保正确处理了线程池中任务的异常,如果任务通过 execute 提交,那么出现异常会导致线程退出,大量的异常会导致线程重复创建引起性能问题,我们应该尽可能确保任务不出异常,同时设置默认的未捕获异常处理程序来兜底;

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python学习2之sublime text编辑器安装配置使用

1、在windows系统中使用sublime text 下载地址 https://www.sublimetext.com/3 2、在sublime text中运行python程序 代码运行可选择菜单Tools->Build或者按CtrlB 3、定制sublime text的设置 3.1将制表符转换为空格 选择菜单view->indentation,核实选择了复选框indent u…

【1++的数据结构】之AVL树

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的数据结构】 文章目录 一&#xff0c;什么是AVL树二&#xff0c;AVL树的插入三&#xff0c;AVL树的旋转3.1 向左旋转3.2 向右旋转3.3 左右双旋3.4 右左双旋 四&#xff0c;验证AVL树是否平衡 …

前端学习之轮播图

前端学习之轮播图 该案例涉及到定时器的使用&#xff0c;元素的活获取&#xff0c;函数的调用等知识的运用 显示图如下&#xff1a;可以点击图标跳转图片&#xff0c;也可以自动轮播 源码如下 <!DOCTYPE html> <html><head><meta charset"UTF-8&q…

监控抽烟检测识别算法

监控抽烟检测识别算法采用yolov7系列网络模型深度学习图像识别技术&#xff0c;监控抽烟检测识别算法能够准确识别人员抽烟的动作和烟雾&#xff0c;监控抽烟检测识别算法一旦发现有人员在禁烟区域内抽烟&#xff0c;将立即触发预警。YOLO的结构非常简单&#xff0c;就是单纯的…

正中优配:“核污染防治”炒作按下暂停键, 中电环保、建龙微纳大跌

连日大涨的核污染防治炒作“步伐”放缓。 8月29日上午&#xff0c;核污染防治概念股多数低开&#xff0c;截止到午间休市&#xff0c;此前4个交易日累计涨超80%的中电环保&#xff08;300172.SZ&#xff09;大跌6.76%。 8月28日晚间&#xff0c;中电环保发布异动公告&#xff…

遗传算法决策变量降维的matlab实现

1.案例背景 1.1遗传算法概述 遗传算法是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。它最初由美国Michigan大学的J. Holland教授提出,1967年, Holland 教授的学生 Bagley在其博士论文中首次提出了“遗传…

三电平离网逆变器接不平衡负载仿真

文章目录 **0、系统框图****1、纯阻性负载R****2、纯感性负载L****3、纯容性负载C****4、 RL负载****5、RC负载****6、CL负载****7、RLC负载** 0、系统框图 闭环控制 PWM调制 逆变桥 LCL滤波电路 负载&#xff08;可配置&#xff09; 坐标变换&#xff08;采样得到&#xff09;…

SAP_ABAP_FUNCTION_ALV案例

SAP ABAP顾问能力模型梳理_企业数字化建设者的博客-CSDN博客SAP Abap顾问能力模型https://blog.csdn.net/java_zhong1990/article/details/132469977 一、Function ALV 1.1 基于退货采购订单创建&#xff0c;解释 FUNCTION_ALV开发的程序结构与代码模板参考 1.2 程序结构 to…

ORA-00604 ORA-00069报错

在测试环境上删除用户&#xff0c;报错如下 rop user "USR_EOS" cascade; * ERROR at line 1: ORA-00604: error occurred at recursive SQL level 1 ORA-00069: cannot acquire lock -- table locks disabled for T_EMPLOYEE 解决方法 alter table USR_EOS.T_EMPL…

运维架构师:驱动企业运维向高效发展“

运维架构师是高级运维工程师的角色&#xff0c;主要负责运维工作的策划和执行。他们需要熟练掌握开源工具&#xff0c;但更重要的是运用思维来进行运维工作&#xff0c;实现DevOps理念和解决各种企业运营中的挑战。他们的职责包括设计各种运维解决方案&#xff0c;例如自动化代…

Python框架【模板继承、继承模板实战、装饰器、蓝图(介绍、单文件、目录结构、模版文件、静态文件 url_for函数子域名实现)】(五)

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小王&#xff0c;CSDN博客博主,Python小白 &#x1f4d5;系列专栏&#xff1a;python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 &#x1f4e7;如果文章知识点有错误…

深入解析SNMP协议及其在网络设备管理中的应用

SNMP&#xff08;Simple Network Management Protocol&#xff0c;简单网络管理协议&#xff09;作为一种用于网络设备管理的协议&#xff0c;在实现网络设备的监控、配置和故障排除方面发挥着重要的作用。本文将深入解析SNMP协议的工作原理、重要概念和功能&#xff0c;并探讨…

回首一路坎坷,献给刚入测试的你

恍惚间&#xff0c;已经进入测试岗位四年多的时间了&#xff0c;回顾过往&#xff0c;思绪良多&#xff0c;一路走来&#xff0c;或多或少的经历了一些坎坷&#xff0c;也遇到了不少的困难&#xff0c;也曾四顾迷茫&#xff0c;在此&#xff0c;我结合了自己的一些心得&#xf…

【实训项目】精点考研

1.设计摘要 如果说高考是一次能够改变命运的考试&#xff0c;那么考研应该是另外一次。为什么那么多人都要考研呢&#xff1f;从中国教育在线官方公布是考研动机调查来看&#xff0c;大家扎堆考研的原因大概集中在这6个方面&#xff1a;本科就业压力大&#xff0c;提升竞争力、…

C++ 面试题(一)--C++基础,面向对象,内存管理

目录 1.part1 C基础 1 C特点 2 说说C语言和C的区别 3 说说 C中 struct 和 class 的区别 4 include头文件的顺序以及双引号""和尖括号<>的区别 5 说说C结构体和C结构体的区别 6 导入C函数的关键字是什么&#xff0c;C编译时和C有什么不同&#xff1f; 7…

Tomcat安装及基本使用

1. 什么是Web服务器 Web服务器是一种应用程序&#xff08;软件&#xff09;&#xff0c;它封装了对HTTP协议的操作&#xff0c;使得开发人员无需直接操作协议&#xff0c;从而简化了Web开发。其主要功能是提供网上信息浏览服务。 Web服务器安装在服务器端&#xff0c;我们可以…

JavaWeb 速通JSON

目录 一、JSON快速入门 1.基本介绍 : 2.定义格式 : 3.入门案例 : 二、JSON对象和字符串的相互转换 1.常用方法 : 2.应用实例 : 3.使用细节 : 三、JSON在Java中的使用 1.基本说明 : 2.应用场景 : 2.1 JSON <---> JavaBean 2.2 JSON <---> List 2.3 JSON …

java对时间序列根据阈值进行连续性分片

问题描述&#xff1a;我需要对一个连续的时间戳list进行分片&#xff0c;分片规则是下一个数据比当前数据要大于某一个阈值则进行分片&#xff1b; 解决方式&#xff1a; 1、输入的有顺序的list &#xff0c;和需要进行分片的阈值 2、调用方法&#xff0c;填入该排序的list和阈…

非煤矿山风险监测预警算法 yolov8

非煤矿山风险监测预警算法通过yolov8网络模型深度学习算法框架&#xff0c;非煤矿山风险监测预警算法在煤矿关键地点安装摄像机等设备利用智能化视频识别技术&#xff0c;能够实时分析人员出入井口的情况&#xff0c;人数变化并检测作业状态。YOLO的结构非常简单&#xff0c;就…

element-ui el-upload组件 on-remove事件 传自定义参数

element-ui el-upload组件 on-remove事件 传自定义参数 1.vue页面 :on-remove"(file, fileList) > {handleRemove(file, fileList, item.order)}"2.methods方法里面