前言
在Java中,有一个臭名昭著的异常–空指针(NullPointerException),为了防止空指针异常带来的危害,我们通常会在程序中进行大量的判空操作,但是使用if…else语法来写的话往往会使代码显得臃肿不堪。其实,我们可以使用Java中的断言机制来巧妙的解决这个问题,从而达到优化代码的目的。
日常的场景
大家都知道,我们在日常的项目中经常会遇到这样子的场景,例如下面是一个普通的controller,我们根据请求传入的id进行数据库查询。
但是如果这个时候传入的id为空,则会引起程序的异常。
@RestController
public class TestController {
@RequestMapping(value = "/test")
public String test(Long id){
System.out.println("打印出你要寻找的id");
System.out.println("执行操作数据库");
return "你要查找的id是" + id;
}
}
所以,我们不得不引入这样子的一个判断:
@RestController
public class TestController {
@RequestMapping(value = "/test")
public String test(Long id){
// --------- 参数判断 start------------
if (Objects.isNull(id)){
return "输入的id不能为空";
}
if (id < 0){
return "输入的id不能小于0";
}
// --------- 参数判断 end-------------
System.out.println("打印出你要寻找的id");
System.out.println("执行操作数据库");
return "你要查找的id是" + id;
}
}
这是大家常用的场景,但是如果传入的参数多了,我们进行这么多的判断,同样会影响代码的可读性。那么有没有一个更加优雅的方式来解决这个问题呢?
这里就要给大家介绍一下–Java中的断言机制
什么是断言机制?
提到断言机制,大家应该都不陌生,我们在看一下开源的代码或者是看一些别人的测试用例的时候,经常会看到这样子的写法:
?
在写测试用例的时候,我们常常使用 断言来判断我们的输出结果是否符合自己的预期,借此来帮助自己判断程序是否良好的运行。
官方的解释呢,是这样子说的:
编写代码时,我们总是会做出一些假设,断言就是用于在代码中捕捉这些假设。
但是你知道吗?
除了测试用例,我们同样可以在自己的代码中使用断言机制来进行参数判断
在 org.springframework.util中为我们集成了断言的工具类,我们可以使用Assert这个方法来对程序中不满足程序预期的内容进行判断
举个例子,我来改写一下之前的案例:
@RequestMapping(value = "/test2")
public String test2(Long id){
// --------- 参数判断 start------------
Assert.notNull(id, "参数不能为空");
Assert.isTrue(id > 0, "ID必须大于0");
// --------- 参数判断 end-------------
System.out.println("打印出你要寻找的id");
System.out.println("执行操作数据库");
return "你要查找的id是" + id;
}
这样子是不是就清晰多了呢?
那么具体这个 Assert.notNull方法中做了什么呢?我们可以点进他的源码中查看:
public static void notNull(@Nullable Object object, String message) {
if (object == null) {
throw new IllegalArgumentException(message);
}
}
可以看到 Assert.notNull方法是对我们的一个常见异常 IllegalArgumentException(非法参数异常)做出了一个封装
这样子我们通过使用这个封装好的util类就可以对代码进行简化。
但是接下来又会出现一个问题:
假如传入的参数就是非法的,那么会出现什么后果的呢?我们可以试一试:
发送请求:
控制台给出报错。
接口返回了服务端异常。
当然,这样子的结果是我们不希望看到呢,那么有没有更加优雅的解决这个问题的方式呢?
我们可以通过全局异常处理来完成
在Spring的3.2版本中增加了一个注解@ControllerAdvice,这个注解可以帮助我们进行异常拦截。
当然,如果你使用的微服务,同样可以使用AOP来对这个问题进行处理。同样简单且使用。
首先我们来看一下使用@ControllerAdvice注解来进行全局异常处理:
首先写出一个全局异常处理类:GlobalExceptionHandler
然后我们给出一个简易的统一返回Result格式:
接着在GlobalExceptionHandler中添加代码:
@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
/**
* 拦截业务异常,返回业务异常信息
* @param ex
* @return
*/
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
public Result handleIllegalArgumentExceptionError(IllegalArgumentException ex) {
String message = ex.getMessage();
return Result.fail(500, message);
}
}
再次请求,异常就能够被捕获了
Assert的拓展应用
在上面的例子中,我们介绍了Assert的两个方法。
一个是在判断参数是否为null时使用到的 **Assert.notNull()**方法
还有一个是判断参数是否大于0的时候用到的 **Assert.isTrue()**方法
此处需要注意的是,我们查看 **Assert.isTrue()**方法的源码:
public static void isTrue(boolean expression, String message) {
if (!expression) {
throw new IllegalArgumentException(message);
}
}
这个方法里面传递的其实是一个boolean类型的表达式,只要我们写出的是一个能够被Java判断为Boolean类型的表达式,这个方法就能帮助我们判断,但是只有当结果为true的时候才会顺利通过,否则就会引发IllegalArgumentException哦。
但是其实Assert这个工具类里面给我们提供了很多很多其他好用到爆的方法。我来给大家判断几个常用的:
对于字符串的判断
hasLength 方法
这个方法可以帮助我们判断传入的字符串是否为空,通常在项目中我们习惯用 StringUtils这个工具类来判定字符串是否为空,根据判断结果执行相应的操作。但是对于一些强校验的场景,比如需要用户传入字符串搜索,或者根据字符串去搜索数据库等场景,如果字符串为空会引发相应异常,这个使用我们使用 Assert.hasLength方法就可以直接阻断程序的运行并且返回相应的错误提示。
用法示例:
@Test
void testHasLength() {
String emptyString = "";
String normalString = "这是用户需要搜索的字符";
// 正常通过
Assert.hasLength(normalString,"输入的字符串不能为空");
// 报出异常,返回提示 "输入的字符串不能为空"
Assert.hasLength(emptyString,"输入的字符串不能为空");
}
hasText 方法
这个方法用来判断传入的参数中是否包含字符,其实主要是为了验证空格的。如果用户传来的参数中只有一串空格,但是没有任何实际的字符,同样会引发异常的。
用法示例:
@Test
void testHasText() {
String notContainText = " ";
String containsText = "这是用户需要搜索的字符";
// 正常通过
Assert.hasText(containsText,"输入的字符不包含字符串");
// 报出异常,返回提示 "输入的字符串不能为空"
Assert.hasText(notContainText,"输入的字符不包含字符串");
}
doesNotContain 方法
这个方法和上面的判断是相反的,用来判断传入的参数中是否 不包含某个字符串
用法示例:
@Test void testDoesNotContain() { String targetStr = "abc"; // 正常通过 Assert.doesNotContain(targetStr,"123","不包含目标字符"); // 提示异常:"不包含必须的字符" Assert.doesNotContain(targetStr,"a","不包含目标字符"); }
集合类
notEmpty 方法
这个方法主要来判定集合类是否为空的情形,并且其中支持了多种数据结构,数组、Map、Collections集合都可以做出判断,接下来给出用法示例:
@Test void testNotEmpty() { ArrayList<Integer> emptyList = new ArrayList<>(); List<Integer> containValueList = List.of(1, 2, 3); HashMap<Integer, Integer> emptyMap = new HashMap<>(); Map<Integer, Integer> containValueMap = Map.of(1, 2, 3, 4); Integer[] emptyArray = new Integer[]{}; Integer[] containValueArr = {1,2,3}; // 正常通过 Assert.notEmpty(containValueList,"list不能为空"); Assert.notEmpty(containValueMap,"map不能为空"); Assert.notEmpty(containValueArr,"array不能为空"); // 提示异常 Assert.notEmpty(emptyList,"list不能为空"); Assert.notEmpty(emptyMap,"map不能为空"); Assert.notEmpty(emptyArray,"array不能为空"); }
还有一个比较特殊的状态判断
state 方法
这个方法在用法上,和我们的 Assert.isTrue是一样的,我们可以看一下它的底层实现:
public static void state(boolean expression, String message) { if (!expression) { throw new IllegalStateException(message); } }
同样是一个表达式,但是区别呢?是它抛出的异常不同,Assert.isTrue抛出的是IllegalArgumentException非法参数异常,而 Assert.state方法抛出的是IllegalStateException非法状态异常。
那么对于一些需要判断状态的场景,我们就可以用到这个方法
用法示例:
public void testState() { Assert.state(this.state.equals("end"), "状态异常,此时的状态不应该为结束"); // ...}
由于之前我们使用了统一异常处理来拦截异常,那么针对这种场景,我们可以进行一个扩展,回到GlobalExceptionHandler这个全局异常处理类中。
我们添加这样一段代码,对于状态异常进行一个捕获,并且给出状态异常一个特定的标识码,例如文中的1005
/** * 拦截业务异常,返回业务异常信息 * @param ex * @return */ @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) public Result handleIllegalStateExceptionExceptionError(IllegalArgumentException ex) { String message = ex.getMessage(); // 此处添加自定义异常码,1005 是状态异常的标识 return Result.fail(1005, message); }
总结
通过使用Java中的断言机制,我们可以更好的简化if判断,对于一些明显异常的参数,明显异常的状态,及时中止并且返回相应的提示,增加了代码的可读性,同时也简化了书写。
|