使用 Assert 优雅的替换 Throw Exception

HYF Lv3

在软件开发过程中,处理各种异常是不可避免的,代码中常常充斥着大量的 try {...} catch (Exception e) {...} 代码块。这种异常处理方式虽然十分有效,但却往往导致代码中出现大量冗余,不仅影响了代码的可读性,还增加了维护的难度。

为了解决这一问题,我们可以考虑使用 Assert 语句来替代部分 throw异常处理。Assert 语句能够帮助我们更优雅地进行前置条件、后置条件和不变量的检查,从而提高代码的简洁性和可读性。在本文中,我们将深入探讨 Assert 的使用场景和方法,并通过具体的实例来展示如何在实际开发中用 Assert 替代 throw 异常处理,以编写更清晰、更可靠的代码。

什么是异常

异常是指在程序执行过程中出现的意外情况或错误,这些情况或错误会导致程序无法按照预期继续运行。在 Java 中,所有异常都继承自 Throwable 类,其中 ExceptionError 是两个直接子类。Exception 类下又分为 RuntimeException(运行时异常)和编译时异常。Java 异常类层次结构的示意图类似这样:

1
2
3
4
java.lang.Throwable
-- java.lang.Exception
-- java.lang.RuntimeException
-- java.lang.Error

常见的异常处理方式

Java 中,处理异常的方式一般有以下几种:try-catch(-finally) 语句、throw 语句和自定义异常。

try-catch语句

try-catch 语句是 Java 中最基本的异常处理方式。它用于捕获在 try 块中可能抛出的异常,并在 catch 块中处理这些异常。

1
2
3
4
5
6
7
try {
// 可能抛出异常的代码
} catch (ExceptionType e) {
// 处理异常的代码
} finally {
// 可选的finally块,用于执行一些清理操作,例如某些资源或者流的关闭
}
try-catch 语句可以捕获并处理多种类型的异常,并且可以使用 finally 块来执行一些无论是否发生异常都要执行的代码,如资源释放操作。

throw语句

throw 语句用于显式地抛出一个异常。它可以用于在方法内部根据某些条件主动抛出异常,从而中断方法的正常执行流程。

1
2
3
if (condition) {
throw new ExceptionType("异常信息");
}
throw 语句通常与异常对象一起使用,通过创建异常对象并抛出它,开发者可以在程序运行时动态地生成异常,从而灵活地处理错误情况。

自定义异常

自定义异常是指开发者根据特定的业务需求,自行定义的异常类。自定义异常类通常继承自 ExceptionRuntimeException
例如下列代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* @author -侑枫
* @date 2024/7/8 23:51:23
*/
@Getter
@AllArgsConstructor
public enum ExceptionTypeEnum {
/**
* 枚举值
*/
PARAMS("1", "参数错误"),
CHECK("2", "校验错误"),
BUSINESS("3", "业务错误"),
LIMITS_AUTHORITY("4", "权限错误"),
UNIQUENESS("5", "唯一性错误"),
SYSTEM("6", "系统错误");

private final String value;
private final String name;

private static final Map<ExceptionTypeEnum, String> KEY_MAP = new EnumMap<>(ExceptionTypeEnum.class);

static {
for (ExceptionTypeEnum item : ExceptionTypeEnum.values()) {
KEY_MAP.put(item, item.getValue());
}
}

public static String fromEnum(ExceptionTypeEnum typeEnum) {
return KEY_MAP.get(typeEnum);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author -侑枫
* @date 2023/2/16 16:23:08
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode(callSuper=true)
public class GlobalException extends RuntimeException {
/**
* 类型
*/
private ExceptionTypeEnum typeEnum;
/**
* 信息
*/
private String msg;

public String getResMsg() {
String typeName = "未知异常";
if (typeEnum != null) {
typeName = typeEnum.getName();
}
return String.format("%s:%s", typeName, msg);
}
}
自定义异常可以提供更具体、更符合业务需求的错误信息,有助于更精确地定位和处理特定的异常情况。通过自定义异常,开发者可以提高代码的可读性和可维护性。

用 Assert 优雅替换 Throw Exception

Assert 是一个用于验证参数和条件的辅助类,通常在开发过程中用于确保代码的前置条件、后置条件和不变量。通过使用 Assert 类,开发者可以简化代码中的验证逻辑,提高代码的可读性和可靠性。

那么 Assert 到底做了哪些事呢?我们来观察一下源码:
Assert类1
Assert类2
Assert类3

从这段代码中可以看到,Assert 其实将 if () {throw new Exception()} 进行了简洁的封装。这种简化不仅提升了代码的可读性和维护性,也显著提高了编码体验。

那么,我们能否借鉴 org.springframework.util.Assert 的设计,编写一个自定义的断言类,而在断言失败时抛出我们自己定义的异常,而非 IllegalArgumentException等内置异常呢?下面我们来尝试实现这一目标。

定义获取错误代码和消息的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author -侑枫
* @date 2024/7/9 0:47:01
*/
public interface IResponseMsg {
/**
* 编码
* @return 返回编码
*/
Integer getCode();

/**
* 信息
* @return
*/
String getMessage();
}

自定义异常类,接受 IResponseMsg 类型的参数,用于格式化异常消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @author -侑枫
* @date 2024/7/9 1:02:43
*/
public class GlobalException extends RuntimeException {
private static final long serialVersionUID = 1L;

public GlobalException(IResponseMsg responseMsg, Object... args) {
super(ObjectUtils.isNotEmpty(args) ? formatMessage(responseMsg, args) : responseMsg.getMessage());
}

public GlobalException(IResponseMsg responseMsg, Object[] args, Throwable cause) {
super(ObjectUtils.isNotEmpty(args) ? formatMessage(responseMsg, args) : responseMsg.getMessage(), cause);
}

public static String formatMessage(IResponseMsg responseMsg, Object... args) {
StringBuilder sb = new StringBuilder(responseMsg.getMessage());
sb.append(": ");
for (int i = 0; i < args.length; i++) {
sb.append(args[i]);
if (i < args.length - 1) {
sb.append(", ");
}
}
return sb.toString();
}

}

定义断言方法和异常创建方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
* @author -侑枫
* @date 2024/7/9 1:19:01
*/
public interface Assert {

/**
* 单参数异常创建方法
*
* @param args 可选可变长度参数(信息)
* @return 异常类
*/
GlobalException newException(Object... args);

/**
* 双参数异常创建方法
*
* @param t 异常类
* @param args 可选可变长度参数(信息)
* @return 异常类
*/
GlobalException newException(Throwable t, Object... args);

/**
* 断言表达式为真
*
* @param expression 断言表达式
* @param args 断言失败时的参数信息
*/
default void state(boolean expression, Object... args) {
if (!expression) {
throw newException(args);
}
}

/**
* 断言字符串长度大于0
*
* @param text 待检查的字符串
* @param args 断言失败时的参数信息
*/
default void hasLength(@Nullable String text, Object... args) {
if (!StringUtils.hasLength(text)) {
throw newException(args);
}
}

/**
* 断言字符串不包含指定子字符串
*
* @param textToSearch 待检查的字符串
* @param substring 指定的子字符串
* @param args 断言失败时的参数信息
*/
default void doesNotContain(@Nullable String textToSearch, String substring, Object... args) {
if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) &&
textToSearch.contains(substring)) {
throw newException(args);
}
}

/**
* 断言字符串包含文本
*
* @param text 待检查的字符串
* @param args 断言失败时的参数信息
*/
default void hasText(@Nullable String text, Object... args) {
if (!StringUtils.hasText(text)) {
throw newException(args);
}
}

/**
* 检查一个类是否可以分配给另一个类
*
* @param superType 超类或接口的 Class 对象
* @param subType 子类或实现类的 Class 对象
* @param message 断言失败时的错误消息
*/
default void isAssignable(Class<?> superType, @Nullable Class<?> subType, String message) {
notNull(superType, "Supertype to check against must not be null");
if (ObjectUtils.isEmpty(superType) || !superType.isAssignableFrom(subType)) {
assignableCheckFailed(superType, subType, message);
}
}

/**
* 断言对象为空
*
* @param object 待检查的对象
* @param args 断言失败时的参数信息
*/
default void isNull(@Nullable Object object, Object... args) {
if (ObjectUtils.isNotEmpty(object)) {
throw newException(args);
}
}

/**
* 断言对象不为空
*
* @param object 待检查的对象
* @param args 断言失败时的参数信息
*/
default void notNull(@Nullable Object object, Object... args) {
if (ObjectUtils.isEmpty(object)) {
throw newException(args);
}
}

/**
* 断言表达式为真
*
* @param expression 断言表达式
* @param args 断言失败时的参数信息
*/
default void isTrue(boolean expression, Object... args) {
if (!expression) {
throw newException(args);
}
}

/**
* 检查数组中是否没有空元素
*
* @param array 待检查的数组
* @param args 断言失败时的参数信息
*/
default void noNullElements(@Nullable Object[] array, Object... args) {
if (ObjectUtils.isNotEmpty(array)) {
for (Object element : array) {
if (ObjectUtils.isEmpty(element)) {
throw newException(args);
}
}
}
}

/**
* 检查集合中是否没有空元素
*
* @param collection 待检查的集合
* @param args 断言失败时的参数信息
*/
default void noNullElements(@Nullable Collection<?> collection, Object... args) {
if (!CollectionUtils.isEmpty(collection)) {
for (Object element : collection) {
if (ObjectUtils.isEmpty(element)) {
throw newException(args);
}
}
}
}

/**
* 断言数组不为空
*
* @param array 待检查的数组
* @param args 断言失败时的参数信息
*/
default void notEmpty(@Nullable Object[] array, Object... args) {
if (org.springframework.util.ObjectUtils.isEmpty(array)) {
throw newException(args);
}
}

/**
* 断言集合不为空
*
* @param collection 待检查的集合
* @param args 断言失败时的参数信息
*/
default void notEmpty(@Nullable Collection<?> collection, Object... args) {
if (CollectionUtils.isEmpty(collection)) {
throw newException(args);
}
}

/**
* 断言映射不为空
*
* @param map 待检查的映射
* @param args 断言失败时的参数信息
*/
default void notEmpty(@Nullable Map<?, ?> map, String... args) {
if (CollectionUtils.isEmpty(map)) {
throw newException(args);
}
}

/**
* 检查类的分配失败
*
* @param superType 超类或接口的 Class 对象
* @param subType 子类或实现类的 Class 对象
* @param msg 错误消息
*/
default void assignableCheckFailed(Class<?> superType, @Nullable Class<?> subType, @Nullable String msg) {
String result = "";
boolean defaultMessage = true;
if (StringUtils.hasLength(msg)) {
if (endsWithSeparator(msg)) {
result = msg + " ";
} else {
result = messageWithTypeName(msg, subType);
defaultMessage = false;
}
}
if (defaultMessage) {
result = result + (subType + " is not assignable to " + superType);
}
throw newException(result);
}

/**
* 检查消息是否以分隔符结尾
*
* @param msg 待检查的消息
* @return 消息是否以分隔符结尾
*/
static boolean endsWithSeparator(String msg) {
return (msg.endsWith(":") || msg.endsWith(";") || msg.endsWith(",") || msg.endsWith("."));
}

/**
* 消息带有类型名称
*
* @param msg 待检查的消息
* @param typeName 类型名称
* @return 带有类型名称的消息
*/
static String messageWithTypeName(String msg, @Nullable Object typeName) {
return msg + (msg.endsWith(" ") ? "" : ": ") + typeName;
}
}
(以上方法是本人亲自从 org.springframework.util.Assert 改的,整理了很久,累死, 上述方法涵盖了开发过程中大部分场景需要用到的方法。本来打算是从 cn.hutool.core.lang.Assert 中改一份,后面发现方法大差不差,就没有重复添加了)

继承 Assert 和 IResponseMsg 接口,并提供默认的异常创建方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @author -侑枫
* @date 2024/7/9 2:12:07
*/
public interface GlobalExceptionAssert extends Assert, IResponseMsg {
/**
* 创建新的全局异常对象
*
* @param args 可选可变长度参数(信息)
* @return 新的全局异常对象
*/
@Override
default GlobalException newException(Object... args) {
return new GlobalException(this, args);
}

/**
* 创建新的全局异常对象,并关联异常类
*
* @param t 异常类
* @param args 可选可变长度参数(信息)
* @return 新的全局异常对象
*/
@Override
default GlobalException newException(Throwable t, Object... args) {
return new GlobalException(this, args, t);
}
}

实现了 GlobalExceptionAssert 接口,每个枚举常量都包含错误代码和值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @author -侑枫
* @date 2024/7/8 2:24:23
*/
@Getter
@AllArgsConstructor
public enum ExceptionTypeEnum implements GlobalExceptionAssert {
/**
* 枚举值
*/
PARAMS("1", "参数错误"),
CHECK("2", "校验错误"),
BUSINESS("3", "业务错误"),
LIMITS_AUTHORITY("4", "权限错误"),
UNIQUENESS("5", "唯一性错误"),
SYSTEM("6", "系统错误");

private final String value;
private final String message;

@Override
public Integer getCode() {
return Integer.parseInt(value);
}

@Override
public String getMessage() {
return message;
}
}

创建 异常处理器类,用于全局处理应用程序中抛出的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @author -侑枫
* @date 2024/7/9 2:33:40
*/
@ControllerAdvice
public class ExceptionTranslator {
/**
* 异常处理方法,处理 Throwable 类型的异常。
* 将异常信息包装成错误消息,返回一个包含该错误消息和 HTTP 状态码的 ResponseEntity 对象。
*
* @param e 异常对象
* @return ResponseEntity 包含错误消息和 HTTP 状态码的响应对象
*/
@ExceptionHandler(Throwable.class)
public ResponseEntity handleException(Throwable e) {
String errMsg = "错误: " + e.getMessage();
return new ResponseEntity(errMsg, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

结果展示

首先我们来尝试一下没有错误的情况
1
2
3
4
5
6
7
8
9
10
@GetMapping
public String test() {
ExceptionTypeEnum.SYSTEM.notNull("123");
System.out.println("notNull");
ExceptionTypeEnum.SYSTEM.isNull(null);
System.out.println("isNull");
ExceptionTypeEnum.SYSTEM.doesNotContain("abc", "aabcde");
System.out.println("doesNotContain");
return "成功!";
}
使用 PostMan 测试

postman测试1

代码1

可以发现结果如我们预期,接下来我们改变一下存在错误的情况(模拟第一个正常,第二个错误)
1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping
public String test() {
List<String> list = new ArrayList<>();
ExceptionTypeEnum.SYSTEM.noNullElements(list);
System.out.println("noNullElements");
boolean flag = false;
ExceptionTypeEnum.SYSTEM.isTrue(flag, "阿噢,出了个错误噢!");
System.out.println("isTrue");
Map<String, Object> map = new HashMap<>();
ExceptionTypeEnum.SYSTEM.notEmpty(map, "Map 你也能出错?");
System.out.println("notEmpty");
return "成功!";
}
使用 PostMan 测试

postman测试2

代码2

可以发现结果依旧为我们预期的样子

结语

在本文中,我们深入探讨了 Java 中异常处理的多种方式以及如何使用断言来优化代码结构和提升开发效率。异常处理是软件开发中不可或缺的一部分,通过合理的异常处理机制,我们可以更好地保障程序的稳定性和可靠性。

我们首先介绍了 Java 中常见的异常处理方式,包括 try-catch 块、Throw 异常和自定义异常等。使用枚举类结合 Assert,只需根据特定的异常情况定义不同的枚举实例,就能够针对不同情况抛出特定的异常,不用定义大量的异常类,同时还具备了断言的良好可读性。使用 断言枚举类 相结合的方式,再配合统一异常处理,基本大部分的异常都能够被捕获。

希望本文对您理解和应用 Java 异常处理机制有所帮助。在实际项目中,根据具体需求选择合适的异常处理方式,是保证软件质量和开发效率的关键之一。愿您在今后的开发工作中能够运用这些技术,编写出更加健壮和可靠的 Java 程序!

  • 标题: 使用 Assert 优雅的替换 Throw Exception
  • 作者: HYF
  • 创建于 : 2024-07-09 00:12:58
  • 更新于 : 2024-07-27 21:21:50
  • 链接: https://youfeng.ink/Enum-Assert-5f368446cf69/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。