有关Assert断言大家并不陌生,我们在做单元测试的时候,看业务事务复合预期,我们可以通过断言来校验,断言常用的方法如下:

public class Assert {
/**
* 结果 = 预期 则正确
*/
static public void assertEquals(Object expected, Object actual);
/**
* 结果 != 预期 则正确
*/
static public void assertNotEquals(Object unexpected, Object actual);
/**
* condition == true 则正确
*/
static public void assertTrue(boolean condition);
/**
* condition == false 则正确
*/
static public void assertFalse(boolean condition);
/**
* 永远是错误
*/
static public void fail();
/**
* 结果不为空 则正确
*/
static public void assertNotNull(Object object);
/**
* 结果为空 则正确
*/
static public void assertNull(Object object);
/**
* 两个对象引用 相等 则正确(上面相当于equels 这里类似于使用“==”比较两个对象)
*/
static public void assertSame(Object expected, Object actual);
/**
* 两个对象引用 不相等 则正确
*/
static public void assertNotSame(Object unexpected, Object actual);
/**
* 两个数组相等 则正确
*/
public static void assertArrayEquals(Object[] expecteds, Object[] actuals);
/**
* 这个单独介绍 具体参考博客:https://www.cnblogs.com/qdhxhz/p/13684458.html
*/
public static <T> void assertThat(T actual, Matcher<? super T> matcher);
};

使用断言能让我们编码看去更加的清爽,比如:

   @Test
public void test1() {
Order order = orderDao.selectById(orderId);
Assert.notNull(order, "订单不存在。");
} @Test
public void test2() {
// 另一种写法
Order order = orderDao.selectById(orderId);
if (order == null) {
throw new IllegalArgumentException("订单不存在。");
}
}

这两种方式一对比,是不是明显感觉第一种更优雅,第二种写法则是相对丑陋的 if {...} 代码块。那么 神奇的 Assert.notNull() 背后到底做了什么呢?

下面是 Assert 的部分源码:

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

可以看到,Assert 其实就是帮我们把 if {...} 封装了一下,是不是很神奇。虽然很简单,但不可否认的是编码体验至少提升了一个档次。

那么我们是不是可以模仿Assert,也写一个自定义断言类,不过断言失败后抛出的异常不是IllegalArgumentException 这些内置异常,而是我们自己定义的异常。

下面让我们来尝试一下。

public interface Assert {
/**
* 创建异常
* @param args
* @return
*/
BaseException newException(Object... args); /**
* 创建异常
* @param t
* @param args
* @return
*/
BaseException newException(Throwable t, Object... args); /**
* 断言对象 obj 非空。如果对象 obj 为空,则抛出异常
*
* @param obj 待判断对象
*/
default void assertNotNull(Object obj) {
if (obj == null) {
throw newException(obj);
}
} /**
* 断言对象 obj 非空。如果对象 obj 为空,则抛出异常
* 异常信息 message 支持传递参数方式,避免在判断之前进行字符串拼接操作
*
* @param obj 待判断对象
* @param args message占位符对应的参数列表
*/
default void assertNotNull(Object obj, Object... args) {
if (obj == null) {
throw newException(args);
}
}
}

  1. 这里只给出Assert接口的部分源码,更多断言方法请参考项目的源码。
  2. BaseException 是所有自定义异常的基类。
  3. 在接口中定义默认方法是Java8的新语法。

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

因为业务逻辑中出现的异常基本都是对应特定的场景,比如根据用户id获取用户信息,查询结果为null,此时抛出的异常可能为UserNotFoundException,并且有特

定的异常码(比如7001)和异常信息“用户不存在”。所以具体抛出什么异常,有Assert的实现类决定。

看到这里,你可能会有会有疑问,按照上面的做法,那不是有多少个异常情况,我都要去实现Assert接口,然后在写定义等量的断言类和异常类,这显然是反人类的,

这也没想象中高明嘛。别急,且听我细细道来。

一、善解人意的Enum

自定义异常BaseException有2个属性,即code、message,这样一对属性,有没有想到什么类一般也会定义这2个属性?

没错,就是枚举类。且看我如何将 Enum 和 Assert 结合起来,相信我一定会让你眼前一亮。如下:

public interface IResponseEnum {
int getCode();
String getMessage();
}

我们自定一个业务异常类

/**
* 业务异常
* 业务处理时,出现异常,可以抛出该异常
*
*/
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);
}
}

业务异常断言实现类,它同时继承2个接口,一个是Assert接口,一个是IResponseEnum接口

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 BusinessResponseEnum implements BusinessExceptionAssert { USER_NOT_FOUND(6001, "未查询到用户信息"), ORDER_NOT_FOUND(7001, "未查询到订单信息"), ; /**
* 返回码
*/
private int code;
/**
* 返回消息
*/
private String message; }

这样如果是业务异常,就都可以在BusinessResponseEnum中定义一个新枚举对象就可以了。以后每增加一种异常情况,只需增加一个枚举实例即可,再也不用每一种异常

都定义一个异常类了。接下来看使用如下:

   /**
* 查询用户信息
*/
public User queryDetail(Integer userId) {
final User user = this.getById(userId);
// 校验非空
BusinessResponseEnum.USER_NOT_FOUND.assertNotNull(user);
return user;
}

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

  /**
* 查询用户信息
*/
public User queryDetail(Integer userId) {
final User user = this.getById(userId); if (user == null) {
throw new UserNotFoundException();
// 或者这样
throw new BusinessException(6001, "未查询到用户信息");
}
return user;
}

使用枚举类结合(继承)Assert,只需根据特定的异常情况定义不同的枚举实例,如上面的USER_NOT_FOUND、ORDER_NOT_FOUND,就能够针对不同情况抛出特定的异常

(这里指携带特定的异常码和异常消息),这样既不用定义大量的异常类,同时还具备了断言的良好可读性。

二、验证统一异常处理

在throw抛出异常后,我们就可以统一去处理异常,如果有必要我们可以加入异常日志存入数据库中,有助于后续排查问题。

/**
* 全局异常处理器
*
* @author xub
* @date 2022/2/28 上午10:52
*/
@Slf4j
@Component
@ControllerAdvice
@ConditionalOnWebApplication
public class ViewExceptionResolver {
/**
* 生产环境
*/
private final static String ENV_PROD = "prod"; @Autowired
private UnifiedMessageSource unifiedMessageSource; /**
* 当前环境
*/
@Value("${spring.profiles.active}")
private String profile; /**
* 获取国际化消息
*
* @param e 异常
* @return
*/
public String getMessage(BaseException e) {
String code = "response." + e.getResponseEnum().toString();
String message = unifiedMessageSource.getMessage(code, e.getArgs()); if (message == null || message.isEmpty()) {
return e.getMessage();
} return message;
} /**
* 业务异常
*
* @param e 异常
* @return 异常结果
*/
@ExceptionHandler(value = BusinessException.class)
@ResponseBody
public CommandResult handleBusinessException(BaseException e) {
log.error(e.getMessage(), e);
return CommandResult.ofFail(e.getResponseEnum().getCode(), getMessage(e));
} /**
* 自定义异常
*
* @param e 异常
* @return 异常结果
*/
@ExceptionHandler(value = BaseException.class)
@ResponseBody
public CommandResult handleBaseException(BaseException e) {
log.error(e.getMessage(), e); return CommandResult.ofFail(e.getResponseEnum().getCode(), getMessage(e));
} /**
* 未定义异常
*
* @param e 异常
* @return 异常结果
*/
@ExceptionHandler(value = Exception.class)
@ResponseBody
public CommandResult handleException(Exception e) {
log.error(e.getMessage(), e); if (ENV_PROD.equals(profile)) {
// 当为生产环境, 不适合把具体的异常信息展示给用户, 比如数据库异常信息.
int code = CommonResponseEnum.SERVER_ERROR.getCode();
BaseException baseException = new BaseException(CommonResponseEnum.SERVER_ERROR);
String message = getMessage(baseException);
return CommandResult.ofFail(code, message);
} return CommandResult.ofFail(CommonResponseEnum.SERVER_ERROR.getCode(), e.getMessage());
}
}

三、测试验证

这里请求地址,业务id=10数据库是不存在这个用户,所以会报错。

localhost:8085/user/getUserInfo?userId=10

达到了预期效果。

四、总结

使用 断言枚举类 相结合的方式,再配合统一异常处理,基本大部分的异常都能够被捕获。为什么说大部分异常,因为当引入 spring cloud security 后,还会

有认证/授权异常,网关的服务降级异常、跨模块调用异常、远程调用第三方服务异常等,这些异常的捕获方式与本文介绍的不太一样,不过限于篇幅,这里不做详细说明,

以后会有单独的文章介绍。

项目地址:用Assert(断言)封装异常,让代码更优雅(附项目源码)

感谢

这篇文章给自己提供了很好的思路,基本上按照这个思路往下写的

统一异常处理介绍及实战:https://www.jianshu.com/p/3f3d9e8d1efa

用Assert(断言)封装异常,让代码更优雅(附项目源码)的更多相关文章

  1. JDK8漫谈——代码更优雅

    简介 lambda表达式,又称闭包(Closure)或称匿名方法(anonymous method).将Lambda表达式引入JAVA中的动机源于一个叫"行为参数"的模式.这种模式 ...

  2. JAVA8-让代码更优雅之List排序

    先定义一个实体类 @Data @AllArgsConstructor @NoArgsConstructor public class Human { private String name; priv ...

  3. Lambda表达式, 可以让我们的代码更优雅.

    在C#中, 适当地使用Lambda表达式, 可以让我们的代码更优雅. 通过lambda表达式, 我们可以很方便地创建一个delegate: 下面两个语句是等价的 Code highlighting p ...

  4. CSS 黑魔法小技巧,让你少写不必要的JS,代码更优雅

    首页   登录注册         CSS 黑魔法小技巧,让你少写不必要的JS,代码更优雅 阅读 8113 收藏 927 2017-09-26 原文链接:github.com 腾讯云容器服务CSS,立 ...

  5. Google将数十亿行代码储存在单一的源码库

    过去16年,Google使用一个中心化源码控制系统去管理一个日益庞大的单一共享源码库.它的代码库包含了约10亿个文件(有重复文件和分支)和 3500万行注解,86TB数据,900万唯一源文件中含有大约 ...

  6. 基于AOP的MVC拦截异常让代码更优美

    与asp.net 打交道很多年,如今天微软的优秀框架越来越多,其中微软在基于mvc的思想架构,也推出了自己的一套asp.net mvc 框架,如果你亲身体验过它,会情不自禁的说‘漂亮’.回过头来,‘漂 ...

  7. 【原创】基于.NET的轻量级高性能 ORM - TZM.XFramework 之让代码更优雅

    [前言] 大家好,我是TANZAME.出乎意料的,我们在立冬的前一天又见面了,天气慢慢转凉,朋友们注意添衣保暖,愉快撸码.距离 TZM.XFramework 的首秀已数月有余,期间收到不少朋友的鼓励. ...

  8. 使用Object#tap使代码更优雅

    今天看spree源码的时候经常看到Object#tap方法.以前只知道有这个方法,而且感觉这个方法调试的作用大于实际,今日看来以前的理解应该不够准确. 先看下官方文档上tap的例子 Yields se ...

  9. 代码规范、GitHub提交源码的标准 答题人-杨宇杰

    1.格式与命名规范1.1 缩进 使用Tab缩进,而不是空格键1.2 换行 每行120字符 if,for,while语句只有单句时,如果该句可能引起阅读混淆,需要用" {"和&quo ...

随机推荐

  1. python基础详解

    python基础部分 python基础一 python基础二 Python最详细,最深入的代码块小数据池剖析 深浅copy python文件操作 python函数部分 python函数初识 pytho ...

  2. 《Spring Boot 实战纪实》之过滤器

    导航 什么是过滤器 Spring的过滤器 Filter定义 过滤的对象 典型应用 过滤器的使用 Filter生命周期 过滤器链 自定义敏感词过滤器 新增自定义过滤器 添加 @WebFilter注解 添 ...

  3. Luogu P1314 [NOIP2011 提高组] 聪明的质监员

    P1314 [NOIP2011 提高组] 聪明的质监员 题意 题目描述 给定\(n\)个物品,给定每个物品的 重量 \(w_i\) 和 价值 \(v_i\) 给定一个标准值 \(s\) 以及一个参数 ...

  4. CSS Modules 的六种用法

    一.局部作用域 二.全局作用域 三.定制哈希类名 四. Class 的组合 五.输入其他模块 六.输入变量

  5. java.lang.IllegalArgumentException: Failed to register servlet with name 'dispatcher'.Check if there is another servlet registered under the same name

    前言 一年前接手了一个项目,项目始终无法运行,不管咋样,都无法处理,最近,在一次尝试中,终于成功处理了. 含义 意思很明显了,注册了一个相同的dispatcher,可是找了很久,没有相同的Contro ...

  6. JScrollPane 自动跟进 自动到滚动到最底部

    感谢大佬:https://blog.csdn.net/csdn_lqr/article/details/51068423 注:以下方法为网上摘抄 1 . JTable( 放在JScrollPane中  ...

  7. 为什么重写hashCode()和equals()方法

    原创:转载需注明原创地址 https://www.cnblogs.com/fanerwei222/p/11477229.html 这两个方法可能大多数新手都没重写过,为什么要重写更是不知道了,所以这里 ...

  8. HTML笔记整理--下节

    欢迎来到HTML基础笔记下节部分! 内联样式 当特殊的样式需要应用到个别元素时,就可以使用内联样式. 使用内联样式的方法是在相关的标签中使用样式属性.样式属性可以包含任何 CSS 属性.以下实例显示出 ...

  9. ssh一段时间无操作后自动断开连接(假死)问题

    平时使用ssh远程服务器的时候(注:远程虚拟机一般不会有这个问题),一段时间没有操作,ssh连接就会处于假死状态,以至于需要重新进行ssh连接,不管你用的什么远程工具都会出现这个问题,那么通过心跳检测 ...

  10. Note -「线性规划」学习笔记

    \(\mathcal{Definition}\)   线性规划(Linear Programming, LP)形式上是对如下问题的描述: \[\operatorname{maximize}~~~~z= ...