替代trycatch处理异常的优雅方式

author author     2022-12-05     735

关键词:

前言

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

一、什么是统一异常处理?

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

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

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

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

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

替代try

二、统一异常处理实战

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

用 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)
throw new 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 assertNotNull(Object obj, Object... args)
if (obj == null)
throw newException(args);

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

善解人意的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();
// 或者这样
throw new BusinessException(7001, "Bad licence type.");

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

统一返回结果

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

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

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

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

验证统一异常处理

因为这一套统一异常处理可以说是通用的,所有可以设计成一个 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 LicenceAddRespData addLicence(LicenceAddRequest request)
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);


小结

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

总结

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

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

从阿里跳槽来的工程师,写个trycatch的方式都这么优雅!(代码片段)

背景软件开发过程中,不可避免的是需要处理各种异常,就我自己来说,至少有一半以上的时间都是在处理各种异常情况,所以代码中就会出现大量的try...catch...finally... 代码块,不仅有大量的冗余代码,... 查看详情

从阿里跳槽来的工程师,写个trycatch的方式都这么优雅!(代码片段)

背景软件开发过程中,不可避免的是需要处理各种异常,就我自己来说,至少有一半以上的时间都是在处理各种异常情况,所以代码中就会出现大量的try...catch...finally... 代码块,不仅有大量的冗余代码,... 查看详情

js不使用trycatch,优雅地处理await报错的情况

constres=awaitjiekou(params);if(res.code===0){//执行代码}else{//错误处理}类似场景景:如果jiekou报500,那不会走到下面的if判断,那相应的代码都不会执行。解决方式://写一个方法去执行原本的操作,如果有错误返回[err],没有错误返回[null... 查看详情

callbackpromisegeneratorasync-await和异常处理的演进

...理的优雅性随着规范的进步越来越高,不要害怕使用 trycatch,不能回避异常处理。我们需要一个健全的架构捕获所有同步、异步的异常。业务方不处理异常时,中断函数执行并启用默认处理,业务方也可以随时捕获异常自己... 查看详情

处理 PostgreSQL 异常的优雅方式?

】处理PostgreSQL异常的优雅方式?【英文标题】:ElegantwayofhandlingPostgreSQLexceptions?【发布时间】:2015-04-1019:04:50【问题描述】:在PostgreSQL中,我想创建一个安全包装机制,如果发生异常,它会返回空结果。考虑以下几点:SELECT*FROM... 查看详情

《全局异常捕获》劝劝潘子吧,别再用trycatch来处理异常了(代码片段)

...的处理非常不优雅,有些接口中为了处理异常使用的trycatch语句占用了一半的代码,读起来灰常头疼,所以阿洛就用一招武林中失传已久的《全局异常捕获大法》一顿输出,处理完后的代码逻辑结构看起来很是清... 查看详情

导入 Python 库并在它们不可用时优雅地处理

...题描述】:我想导入一堆库并捕获异常。如果我只有1个trycatch块,我会得到1个异常(第一个)。是否有一种模式可以遍历所有库并为每个缺少的库提供单独的异常?#!/usr/bin/envpyt 查看详情

trycatch处理流的异常

1、trycatch处理异常trycatch(Exceptione)finally必然执行的代码,一般是释放资源2、流使用trycatch处理异常 其中,变量作用域只在当前方法体即大括号范围,为创建流对象变量要放在try方法体外面,初始化为null;变量创建时候尽量... 查看详情

求trycatch语法

C#中改怎么写捕获用户输入的信息错误比如说用户输入的不能是除0错误呢,判断代码写在哪用户输入的信息错误try用户输入的信息的代码catch(Exceptionex)处理异常的代码finally不管有没有异常,都得执行的代码Exception可由下面的异常... 查看详情

springboot统一参数校验统一异常统一响应,这才是优雅的处理方式!(代码片段)

前言本篇主要要介绍的就是controller层的处理,一个完整的后端请求由4部分组成:接口地址(也就是URL地址)请求方式(一般就是get、set,当然还有put、delete)请求数据(request,有head跟body)响应数据(response)本篇将解决以... 查看详情

为啥不建议用trycatch

一句话解释:trycatch机制非常好。那些觉得trycatch不行的人,是他们自己的水平有问题,无法理解这种机制。并且这群人写代码不遵守规则,喜欢偷懒,这才造成trycatch不好的错觉。详细解释:1.程序要健壮,必须要设计报错机制... 查看详情

求求你们了,别再写满屏的trycatch了!(代码片段)

...码属于哪一种风格?然后哪种编码风格您更喜欢?丑陋的trycatch代码块优雅的Controller上面的示例 查看详情

scala的异常处理trycatch(代码片段)

objectTestdefmain(args:Array[String])tryvalf=newFileReader("input.txt")catchcaseex:FileNotFoundException=>println("Missingfileexception")caseex:IOException=>println("IOException")  查看详情

scala的异常处理trycatch(代码片段)

objectTestdefmain(args:Array[String])tryvalf=newFileReader("input.txt")catchcaseex:FileNotFoundException=>println("Missingfileexception")caseex:IOException=>println("IOException")&nbs 查看详情

struts2的异常处理机制

...定逻辑视图名。这种方式非常繁琐,需要在方法中写大量trycatch块,最大的缺点还是一旦需要改变异常处理方法时,需要修改代码。最好的方式是通过声明式的方式管理异常处理。struts2提供了一种声明式的异常处理方式。一、原... 查看详情

java编程trycatch的语法使用格式

Java中有两种处理异常的方式,分别是用throws抛出异常、用try、catch捕获异常。try-catch在Javatry-catch语句的语法格式:当try语句当中捕获到异常时,会将异常抛出到catch语句中匹配异常类型,当异常和catch后面的异常类型相匹配时,... 查看详情

使用优雅方式对参数验证进行处理

...等等,都应该给出异常或错误提示信息。这个参数的验证处理有多种方式,最为简单的方式就是使用条件语句对参数进行判断,这样的判断代码虽然容易理解,但比较臃肿,如果对多个参数、多个条件进行处理,那么代码就非常... 查看详情

solidity极简入门#30.trycatch

try-catch是现代编程语言几乎都有的处理异常的一种标准方式,solidity0.6版本也添加了它。这一讲,我们将介绍如何利用try-catch处理智能合约中的异常。try-catch在solidity中,try-catch只能被用于external函数或创建合约时constructor(被视... 查看详情