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

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了替代try catch处理异常的优雅方式相关的知识,希望对你有一定的参考价值。

前言

软件开发过程中,不可避免的是需要处理各种异常,就我自己来说,至少有一半以上的时间都是在处理各种异常情况,所以代码中就会出现大量的​​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 后,还会有认证/授权异常,网关的服务降级异常、跨模块调用异常、远程调用第三方服务异常等,这些异常的捕获方式与本文介绍的不太一样。

以上是关于替代try catch处理异常的优雅方式的主要内容,如果未能解决你的问题,请参考以下文章

从阿里跳槽来的工程师,写个try catch的方式都这么优雅!

从阿里跳槽来的工程师,写个try catch的方式都这么优雅!

还在滥用try-catch处理异常?看看springboot的优雅实现吧

减少 try-catch ,这样做才叫优雅!

减少 try-catch ,这样做才叫优雅!

比多个 Catch 块更优雅的异常处理? [复制]