Skip to main content

moregeek program

替代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 catch处理异常的优雅方式_异常处理

二、统一异常处理实战

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

用 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 后,还会有认证/授权异常,网关的服务降级异常、跨模块调用异常、远程调用第三方服务异常等,这些异常的捕获方式与本文介绍的不太一样。

©著作权归作者所有:来自51CTO博客作者码农小宋的原创作品,请联系作者获取转载授权,否则将追究法律责任

java统计用户在线人数_码农小宋的博客-多极客编程

统计用户在线人数在统计用户在人数的时候,我们用到了监听器,监听器大致分为以下三种:​​ServletRequestListener​​:用于监听请求的监听接口​​HttpSessionListener​​:用于监听会话的监听接口​​ServletContextListener​​:用于监听应用的回话接口错误的统计办法监听Request域这种统计办法是错误的认为每次刷新页面后进行进行一次的​​cou

tomcat 调优之从 linux 内核源码层面看 tcp backlog_yanhom-blog的博客-多极客编程

前两天看到一群里在讨论 Tomcat 参数调优,看到不止一个人说通过 accept-count 来配置线程池大小,我笑了笑,看来其实很多人并不太了解我们用的最多的 WebServer Tomcat,这篇文章就来聊下 Tomcat 调优,重点介绍下线程池调优及 TCP 半连接、全连接队列调优。 Tomcat 线程池 先来说下线程池调优,就拿 SpringBoot 内置的 Tomcat 来说,确实是支

mysql存储引擎_wx630f055ce23fc的博客-多极客编程

存储引擎的选择为不同的业务表选择不同的存储引擎,例如:查询操作多的业务表,用 MyISAM。临时数据用 Memeroy。常规的并发大更新多的表用 InnoDB。字段定义原则:使用可以正确存储数据的最小数据类型。为每一列选择合适的字段类型。整数类型INT 有 8 种类型,不同的类型的最大存储范围是不一样的。性别?用 TINYINT,因为 ENUM 也是整数存储。字符类型变长情况下,varchar 更

ssm整合项目实战之一产品管理实战_欢迎来视频课学习的博客-多极客编程

一、课程目标1. 【掌握】SSM整合2. 【√】学习Lombok使用3. 【理解】Layui页面书写(备注:其他前端技术也可以)4. 【理解】理解SSM综合案例表的结构5. 【掌握】产品管理二、SSM整合Spring+SpringMVC+Mybatis-->SSMSpring+Struts2+Hibernate -->SSH2.1 简单整合在学习springmvc时我们发现,spri

远程调用之@feignclient注解属性说明_wx630f055ce23fc的博客-多极客编程

​​Feign​​​是声明性的web服务客户端。它使编写web服务客户端更加容易。通过Feign我们可以实现调用远程服务像调用本地一样便捷。本篇文章主要详细聊聊​​Feign​​​下的一个核心注解​​@FeignClient​​相关属性。通过查阅@FeignClient源码,可以看到它的注解包括以下属性:@Target({ElementType.TYPE})@Retention(Retention

redis常见异常汇总_wx630f055ce23fc的博客-多极客编程

概述在阿里云文档中看到一篇Redis错误排查的内容,作者整理的比较详细,感觉很有参考意义,摘录下来,方便个人后续参考使用...Jedis虽然使用起来比较简单,但是不能根据使用场景设置合理的参数(例如连接池参数),或者不合理地使用了一些功能(例如Lua和事务)时,也会产生很多问题,本文对这些常见问题进行逐一说明。错误概览redis.clients.jedis.exceptions.JedisConn

java统计用户在线人数_码农小宋的博客-多极客编程

统计用户在线人数在统计用户在人数的时候,我们用到了监听器,监听器大致分为以下三种:​​ServletRequestListener​​:用于监听请求的监听接口​​HttpSessionListener​​:用于监听会话的监听接口​​ServletContextListener​​:用于监听应用的回话接口错误的统计办法监听Request域这种统计办法是错误的认为每次刷新页面后进行进行一次的​​cou

tomcat 调优之从 linux 内核源码层面看 tcp backlog_yanhom-blog的博客-多极客编程

前两天看到一群里在讨论 Tomcat 参数调优,看到不止一个人说通过 accept-count 来配置线程池大小,我笑了笑,看来其实很多人并不太了解我们用的最多的 WebServer Tomcat,这篇文章就来聊下 Tomcat 调优,重点介绍下线程池调优及 TCP 半连接、全连接队列调优。 Tomcat 线程池 先来说下线程池调优,就拿 SpringBoot 内置的 Tomcat 来说,确实是支

mysql存储引擎_wx630f055ce23fc的博客-多极客编程

存储引擎的选择为不同的业务表选择不同的存储引擎,例如:查询操作多的业务表,用 MyISAM。临时数据用 Memeroy。常规的并发大更新多的表用 InnoDB。字段定义原则:使用可以正确存储数据的最小数据类型。为每一列选择合适的字段类型。整数类型INT 有 8 种类型,不同的类型的最大存储范围是不一样的。性别?用 TINYINT,因为 ENUM 也是整数存储。字符类型变长情况下,varchar 更

10个python脚本来自动化你的日常任务_mb6242cc9db1000的博客-多极客编程

在这个自动化时代,我们有很多重复无聊的工作要做。想想这些你不再需要一次又一次地做的无聊的事情,让它自动化,让你的生活更轻松。那么在本文中,我将向您介绍 10 个 Python 自动化脚本,以使你的工作更加自动化,生活更加轻松。因此,没有更多的重复任务将这篇文章放在您的列表中,让我们开始吧。01、解析和提取 HTML此自动化脚本将帮助你从网页 URL 中提取 HTML,然后还为你提供可用于解析 HT

【c语言进阶】一.数据的储存_number 5 的博客-多极客编程

通过之前的学习,你已经入门了C语言,那我们就来尝试些小BOSS吧(一)数据类型介绍类型的意义:1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。2.如何看待内存空间的视角整型charunsigned charsigned charshortunsigned short [int]signed short [int]intunsigned intsigned intlongunsigned

ssm整合项目实战之一产品管理实战_欢迎来视频课学习的博客-多极客编程

一、课程目标1. 【掌握】SSM整合2. 【√】学习Lombok使用3. 【理解】Layui页面书写(备注:其他前端技术也可以)4. 【理解】理解SSM综合案例表的结构5. 【掌握】产品管理二、SSM整合Spring+SpringMVC+Mybatis-->SSMSpring+Struts2+Hibernate -->SSH2.1 简单整合在学习springmvc时我们发现,spri