能代替try catch处理异常的优雅方式

news2025/1/12 18:06:27

前言

软件开发过程中,不可避免的是需要处理各种异常,就我自己来说,至少有一半以上的时间都是在处理各种异常情况,所以代码中就会出现大量的try {…} catch {…} finally {…} 代码块,不仅有大量的冗余代码,而且还影响代码的可读性。

  1. 什么是统一异常处理

Spring在3.2版本增加了一个注解@ControllerAdvice,可以与@ExceptionHandler、@InitBinder、@ModelAttribute 等注解注解配套使用。

不过跟异常处理相关的只有注解@ExceptionHandler,从字面上看,就是 异常处理器 的意思,其实际作用也是:若在某个Controller类定义一个异常处理方法,并在方法上添加该注解,那么当出现指定的异常时,会执行该处理异常的方法,其可以使用springmvc提供的数据绑定,比如注入HttpServletRequest等,还可以接受一个当前抛出的Throwable对象。

但是,这样一来,就必须在每一个Controller类都定义一套这样的异常处理方法,因为异常可以是各种各样。这样一来,就会造成大量的冗余代码,而且若需要新增一种异常的处理逻辑,就必须修改所有Controller类了,很不优雅。也可以定义个类似BaseController的基类,这种做法虽然没错,但因为这样的代码有一定的侵入性和耦合性,万一已经继承其他基类了呢。

那有没有一种方案,既不需要跟Controller耦合,也可以将定义的 异常处理器 应用到所有控制器呢?所以注解@ControllerAdvice出现了,简单的说,该注解可以把异常处理器应用到所有控制器,而不是单个控制器。借助该注解,我们可以实现:在独立的某个地方,比如单独一个类,定义一套对各种异常的处理机制,然后在类的签名加上注解@ControllerAdvice,统一对 不同阶段的、不同异常 进行处理。这就是统一异常处理的原理

注意到上面对异常按阶段进行分类,大体可以分成:进入Controller前的异常 和 Service 层异常,具体可以参考下图:

  1. 统一异常处理实战

在定义统一异常处理类之前,先来介绍一下如何优雅的判定异常情况并抛异常。

用 Assert(断言) 替换 throw exception

@Test
public void test1(){
    ...
    User user = userDao.selectById(userId);
    Assert.notNull(user, "用户不存在.");
    ...
}

@Test
public void test2(){
    // 另一种写法
    User user = userDao.selectById(userId);
    if (user == null) {
        thrownew IllegalArgumentException("用户不存在.");
    }
}

有没有感觉第一种判定非空的写法很优雅,其实Assert.notNull()的源码部分也是用的第二种写法。

public abstract class Assert {
    public Assert() {
    }

    public static void notNull(@Nullable Object object, String message) {
        if (object == null) {
            throw new IllegalArgumentException(message);
        }
    }
}

可以看到,Assert 其实就是帮我们把 if {…} 封装了一下,是不是很神奇。虽然很简单,但不可否认的是编码体验至少提升了一个档次。那么我们能不能模仿org.springframework.util.Assert,也写一个断言类,不过断言失败后抛出的异常不是IllegalArgumentException 这些内置异常,而是我们自己定义的异常。

下面让我们来尝试一下:

default void assert NotNull(Object obj, Object... args) {
    if (obj == null) {
        throw new Exception(args);
    }
}

上面的Assert断言方法是使用接口的默认方法定义的,然后有没有发现当断言失败后,抛出的异常不是具体的某个异常,而是交由1个newException接口方法提供。

2.1 善解人意的Enum

自定义异常BaseException有2个属性,即code、message,这样一对属性,有没有想到什么类一般也会定义这2个属性?没错,就是枚举类。将 Enum 和 Assert 结合起来,相信会让你眼前一亮。

public interface IResponseEnum {
    int getCode();
    String getMessage();
}
/**
 * <p>业务异常</p>
 * <p>业务处理时,出现异常,可以抛出该异常</p>
 */
public class BusinessException extends  BaseException {

    private static final long serialVersionUID = 1L;

    public BusinessException(IResponseEnum responseEnum, Object[] args, String message) {
        super(responseEnum, args, message);
    }

    public BusinessException(IResponseEnum responseEnum, Object[] args, String message, Throwable cause) {
        super(responseEnum, args, message, cause);
    }
}
public interface BusinessExceptionAssert extends IResponseEnum, Assert {

    @Override 
    default BaseException newException(Object... args) {
        String msg = MessageFormat.format(this.getMessage(), args);

        return new BusinessException(this, args, msg);
    }

    @Override 
    default BaseException newException(Throwable t, Object... args) {
        String msg = MessageFormat.format(this.getMessage(), args);

        return new BusinessException(this, args, msg, t);
    }

}
@Getter
@AllArgsConstructor
public enum ResponseEnum implements BusinessExceptionAssert {

    /**
     * Bad licence type
     */
    BAD_LICENCE_TYPE(7001, "Bad licence type."),
    /**
     * Licence not found
     */
    LICENCE_NOT_FOUND(7002, "Licence not found.")
    ;

    /**
     * 返回码
     */private int code;
    /**
     * 返回消息
     */private String message;
}

这里代码示例中定义了两个枚举实例:BAD_LICENCE_TYPE、LICENCE_NOT_FOUND,分别对应了BadLicenceTypeException、LicenceNotFoundException两种异常。

以后每增加一种异常情况,只需增加一个枚举实例即可,再也不用每一种异常都定义一个异常类了。然后再来看下如何使用,假设LicenceService有校验Licence是否存在的方法,如下

/**
 * 校验{@link Licence}存在
 * @param licence
 */private void checkNotNull(Licence licence){
    ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
}

若不使用断言,代码可能如下:

private void checkNotNull(Licence licence) {
        if (licence == null) {
            throw new LicenceNotFoundException();
            // 或者这样thrownew BusinessException(7001, "Bad licence type.");
        }
    }

使用枚举类结合(继承)Assert,只需根据特定的异常情况定义不同的枚举实例,如上面的BAD_LICENCE_TYPE、LICENCE_NOT_FOUND,就能够针对不同情况抛出特定的异常(这里指携带特定的异常码和异常消息),这样既不用定义大量的异常类,同时还具备了断言的良好可读性,当然这种方案的好处远不止这些

2.2 统一返回结果

在验证统一异常处理器之前,顺便说一下统一返回结果。说白了,其实是统一一下返回结果的数据结构。code、message 是所有返回结果中必有的字段,而当需要返回数据时,则需要另一个字段 data 来表示。

所以首先定义一个 BaseResponse 来作为所有返回结果的基类;

然后定义一个通用返回结果类CommonResponse,继承 BaseResponse,而且多了字段 data;

为了区分成功和失败返回结果,于是再定义一个 ErrorResponse

2.3 验证统一异常处理

因为这一套统一异常处理可以说是通用的,所有可以设计成一个 common包,以后每一个新项目/模块只需引入该包即可。所以为了验证,需要新建一个项目,并引入该 common包。

@Service
public class LicenceService extends ServiceImpl<LicenceMapper, Licence> {

    @Autowired
    private OrganizationClient organizationClient;

    /**
     * 查询{@link Licence} 详情
     * @param licenceId
     * @return
     */
    public LicenceDTO queryDetail(Long licenceId){
        Licence licence = this.getById(licenceId);
        checkNotNull(licence);

        OrganizationDTO org = ClientUtil.execute(() -> organizationClient.getOrganization(licence.getOrganizationId()));
        return toLicenceDTO(licence, org);
    }

    /**
     * 分页获取
     * @param licenceParam 分页查询参数
     * @return
     */
    public QueryData<SimpleLicenceDTO> getLicences(LicenceParam licenceParam){
        String licenceType = licenceParam.getLicenceType();
        LicenceTypeEnum licenceTypeEnum = LicenceTypeEnum.parseOfNullable(licenceType);
        // 断言, 非空
        ResponseEnum.BAD_LICENCE_TYPE.assertNotNull(licenceTypeEnum);

        LambdaQueryWrapper<Licence> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(Licence::getLicenceType, licenceType);
        IPage<Licence> page = this.page(new QueryPage<>(licenceParam), wrapper);
        return new QueryData<>(page, this::toSimpleLicenceDTO);
    }

    /**
     * 新增{@link Licence}
     * @param request 请求体
     * @return
     */
    @Transactional(rollbackFor = Throwable.class)
    public LicenceAddRespDataaddLicence(LicenceAddRequestrequest) {
        Licence licence = new Licence();
        licence.setOrganizationId(request.getOrganizationId());
        licence.setLicenceType(request.getLicenceType());
        licence.setProductName(request.getProductName());
        licence.setLicenceMax(request.getLicenceMax());
        licence.setLicenceAllocated(request.getLicenceAllocated());
        licence.setComment(request.getComment());
        this.save(licence);

        return new LicenceAddRespData(licence.getLicenceId());
    }

    /**
     * entity -> simple dto
     * @param licence {@link Licence} entity
     * @return {@link SimpleLicenceDTO}
     */
    private SimpleLicenceDTO toSimpleLicenceDTO(Licence licence){
        // 省略
    }

    /**
     * entity -> dto
     * @param licence {@link Licence} entity
     * @param org {@link OrganizationDTO}
     * @return {@link LicenceDTO}
     */
    private LicenceDTO toLicenceDTO(Licence licence, OrganizationDTO org){
        // 省略
    }

    /**
     * 校验{@link Licence}存在
     * @param licence
     */
    private void checkNotNull(Licence licence){
        ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
    }

}

2.4 小结

测试的异常都能够被捕获,然后以 code、message 的形式返回。每一个项目/模块,在定义业务异常的时候,只需定义一个枚举类,然后实现接口 BusinessExceptionAssert,最后为每一种业务异常定义对应的枚举实例即可,而不用定义许多异常类。使用的时候也很方便,用法类似断言。

3. 总结

使用 断言 和 枚举类 相结合的方式,再配合统一异常处理,基本大部分的异常都能够被捕获。

为什么说大部分异常,因为当引入 spring cloud security 后,还会有认证/授权异常,网关的服务降级异常、跨模块调用异常、远程调用第三方服务异常等,这些异常的捕获方式与本文介绍的不太一样。

另外,当需要考虑国际化的时候,捕获异常后的异常信息一般不能直接返回,需要转换成对应的语言,不过本文已考虑到了这个,获取消息的时候已经做了国际化映射,逻辑如下:

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

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

相关文章

【办公类-19-03】办公中的思考——Python批量统一文件名的序号(保教主任整理打印文件)

背景需求&#xff1a;为迎接督导检查&#xff0c;保教主任从各条线收集文本资料。并在每个文件名称前手动编号。但是她嘀咕道&#xff1a;”为什么两套资料放在一个文件里就不是按照数字序号排序&#xff1f;&#xff0c;有的是1X-&#xff0c;有的是40X&#xff0c;看起来很乱…

20230304 CF855 div3 vp

Dashboard - Codeforces Round 855 (Div. 3) - Codeforces呃呃&#xff0c;评价是&#xff0c;毫无进步呃呃呃呃呃呃呃呃呃呃呃呃呃呃呃呃呃该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训了该加训…

有关平方或高次方的公式整理一元高次方程的求解

Part.I Introduction 这篇博文记录一下数学中常用的有关平方或高次方的一些公式。 Chap.I 一些结论 下面一部分汇总了一些重要的结论 完全平方公式&#xff1a;(ab)2a22abb2(ab)^2a^22abb^2(ab)2a22abb2平方差公式&#xff1a;a2−b2(ab)(a−b)a^2-b^2(ab)(a-b)a2−b2(ab)(…

Spring——Bean管理-注解方式进行属性注入

Spring针对Bean管理中创建对象提供的注解有哪些&#xff1f;Component&#xff1a;普通Service&#xff1a;业务逻辑层Controller&#xff1a;controller层Repository&#xff1a;dao层用注解的方式是为什么&#xff1f;简化xml方式开发&#xff0c;只需要注解就可以完成在配置…

【Redis场景5】集群秒杀优化-分布式锁

集群环境下的秒杀问题 前序 【Redis场景1】用户登录注册 【Redis场景2】缓存更新策略(双写一致) 【Redis场景3】缓存穿透、击穿问题 【Redis场景拓展】秒杀问题-全局唯一ID生成策略 【Redis场景4】单机环境下秒杀问题 在单机环境下的并发问题&#xff0c;我们可以使用相关…

39. 实战:基于api接口实现视频解析播放(32接口,窗口化操作,可导出exe,附源码)

目录 前言 目的 思路 代码实现 需要导入的模块 1. 导入解析网站列表&#xff0c;实现解析过程 2. 设计UI界面 3. 设置窗口居中和循环执行 4. 注意事项 完整源码 运行效果 总结 前言 本节将类似34. 实战&#xff1a;基于某api实现歌曲检索与下载&#xff08;附完整…

SpringCloud:Nacos的安装(Windows,Linux)

目录 一、认识和安装Nacos 1、下载 2、点击进入Github&#xff0c;进入Releases 3、点击Tags 4、解压&#xff08;Windows版&#xff09; 5、端口配置 6、启动 7、访问 二、Linux系统安装Nacos 1、打开虚拟机&#xff0c;使用xshell连接虚拟机&#xff0c;Nacos依赖于…

JVM内置锁synchronized关键字详解

目录 JVM内置锁synchronized关键字详解 设计同步器的意义 如何解决线程并发安全问题&#xff1f; synchronized原理详解 synchronized底层原理 synchronized在jdk1.6前后的变化【重点】 jdk小于1.6时 jdk>1.6时 轻量级锁何时升级为重量级锁&#xff1f;&#xff1f;…

【ROS学习笔记10】ROS中配置自定义Cpp头文件和导入自定义Python库

【ROS学习笔记10】ROS中配置自定义Cpp头文件和导入自定义Python库 文章目录【ROS学习笔记10】ROS中配置自定义Cpp头文件和导入自定义Python库一、ROS中的头文件和源文件1.1 自定义头文件调用1.2 自定义源文件调用二、Python模块的导入Reference写在前面&#xff0c;本系列笔记参…

springBoot 启动指定配置文件环境多种方案

springBoot 启动指定配置文件环境理论上是有多种方案的&#xff0c;一般都是结合我们的实际业务选择不同的方案&#xff0c;比如&#xff0c;有pom.xml文件指定、maven命令行指定、配置文件指定、启动jar包时指定等方案&#xff0c;今天我们一一分享一下&#xff0c;以供参考&a…

Java知识复习(十二)Docker

1、容器 一句话概括容器&#xff1a;容器就是将软件打包成标准化单元&#xff0c;以用于开发、交付和部署容器镜像是轻量的、可执行的独立软件包 &#xff0c;包含软件运行所需的所有内容&#xff1a;代码、运行时环境、系统工具、系统库和设置。容器化软件适用于基于 Linux 和…

Redis学习(一):NoSQL概述

为什么要使用Nosql 现在是大数据时代&#xff0c;过大的数据一般的数据库无法进行分析处理了。 单机MySQL的年代 90年代&#xff0c;一个基本的网站访问量一般不会太大&#xff0c;单个数据库完全足够&#xff01; 那个时候&#xff0c;更多的去使用静态网站&#xff0c;服务器…

TD算法超详细解释,一篇文章看透彻!

【已解决】TD算法超详细解释和实现&#xff08;Sarsa&#xff0c;n-step Sarsa&#xff0c;Q-learning&#xff09;一篇文章看透彻&#xff01; 郑重声明&#xff1a;本系列内容来源 赵世钰(Shiyu Zhao)教授的强化学习数学原理系列&#xff0c;本推文出于非商业目的分享个人学习…

DockerFile创建及案例

DockerFile dockerfile是用来构建docker镜像的文件&#xff0c;命令脚本参数脚本&#xff01; 构建步骤 编写一个dockerfile文件docker build 构建成为一个对象docker run 运行镜像docker push 发布镜像&#xff08;DockerHub、阿里云镜像仓库&#xff09; 去官网Docker-Hub…

51单片机——串口通信,小白讲解,相互学习

通讯的基本概念 51单片机不仅可以实现串口通信&#xff0c;还可以通过IO口模拟实现多种其他通信&#xff0c;比如 SPI&#xff0c;IIC等&#xff0c;学习这些通信前&#xff0c;我们很有必要了解下通信的基本概念。通信的方式可以分为多种&#xff0c;按照数据传输方式可分为串…

MySQL——复合查询+表的内外连接

文章目录复合查询基本查询多表查询自连接子查询1、单行子查询2、多行子查询3、多列子查询4、在from子句中使用子查询&#x1f60a;5、合并查询结果unionunion all表的内连和外连内连接外连接左外连接右外连接复合查询 接下来我们就要进行的是多张表里进行查询操作&#xff0c;…

大数据相关面试题

linux 常见linux高级命令&#xff1f; top、iotopnetstatdf -hjmap -heaptarrpmps -efshell 用过的shell工具&#xff1f; awk Awk 命令详解 - 简书 awk是行处理器: 相比较屏幕处理的优点&#xff0c;在处理庞大文件时不会出现内存溢出或是处理缓慢的问题&#xff0c;通常用来…

MySQL中的Join连接查询

目录JoinJoin的分类笛卡尔积笛卡尔积出现的原因为什么不推荐有笛卡尔积出现那应该怎么做多表连接Join的使用小表驱动大表小表驱动大表是什么小表驱动大表的好处如何区分哪一个是驱动表和被驱动表Join原理及算法NLJ算法BNLJ算法总结&#xff1a;如何写入高性能的连接查询为什么M…

一阶低通滤波介绍及simulink模型

一阶低通滤波 背景介绍 低通滤波是一种过滤方式&#xff0c;规定低频信号能正常通过&#xff0c;而超过设定临界值的高频信号则被阻隔、减弱。低通滤波可以简单的认为&#xff1a;设定一个频率点&#xff0c;当信号频率高于这个频率时不能通过&#xff0c;在数字信号中&#…

对象图实例解析

总目录链接>> AutoSAR入门和实战系列总目录 文章目录更快、更好、更轻松地学习 UML对象图的目的对象图一览类到对象图示例 - 订单系统基本对象图符号和符号类图与对象图对象图 - 通过示例学习对象图示例 I - 公司结构对象图示例 II - POS对象图示例 III - Writer对象结构…