当前位置 : 主页 > 编程语言 > java >

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

来源:互联网 收集:自由互联 发布时间:2022-10-26
前言 软件开发过程中,不可避免的是需要处理各种异常,就我自己来说,至少有一半以上的时间都是在处理各种异常情况,所以代码中就会出现大量的​​try {…} catch {…} finally {…}​

前言

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

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

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

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

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

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

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

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

二、统一异常处理实战

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

用 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处理异常的优雅方式_spring_02

上一篇:Hibernate缓存及核心接口类详解
下一篇:没有了
网友评论