使用 Assert 优雅的替换 Throw Exception
在软件开发过程中,处理各种异常是不可避免的,代码中常常充斥着大量的 try {...} catch (Exception e) {...}
代码块。这种异常处理方式虽然十分有效,但却往往导致代码中出现大量冗余,不仅影响了代码的可读性,还增加了维护的难度。
为了解决这一问题,我们可以考虑使用 Assert
语句来替代部分 throw
异常处理。Assert
语句能够帮助我们更优雅地进行前置条件、后置条件和不变量的检查,从而提高代码的简洁性和可读性。在本文中,我们将深入探讨 Assert
的使用场景和方法,并通过具体的实例来展示如何在实际开发中用 Assert
替代 throw 异常处理,以编写更清晰、更可靠的代码。
什么是异常
异常是指在程序执行过程中出现的意外情况或错误,这些情况或错误会导致程序无法按照预期继续运行。在 Java
中,所有异常都继承自 Throwable
类,其中 Exception
和 Error
是两个直接子类。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
语句是 Java
中最基本的异常处理方式。它用于捕获在 try
块中可能抛出的异常,并在 catch
块中处理这些异常。
1 2 3 4 5 6 7
| try { } catch (ExceptionType e) { } finally { }
|
try-catch
语句可以捕获并处理多种类型的异常,并且可以使用 finally
块来执行一些无论是否发生异常都要执行的代码,如资源释放操作。
throw
语句用于显式地抛出一个异常。它可以用于在方法内部根据某些条件主动抛出异常,从而中断方法的正常执行流程。
1 2 3
| if (condition) { throw new ExceptionType("异常信息"); }
|
throw
语句通常与异常对象一起使用,通过创建异常对象并抛出它,开发者可以在程序运行时动态地生成异常,从而灵活地处理错误情况。
自定义异常是指开发者根据特定的业务需求,自行定义的异常类。自定义异常类通常继承自 Exception
或 RuntimeException
例如下列代码:
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
|
@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
|
@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
其实将 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
|
public interface IResponseMsg {
Integer getCode();
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
|
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
|
public interface Assert {
GlobalException newException(Object... args);
GlobalException newException(Throwable t, Object... args);
default void state(boolean expression, Object... args) { if (!expression) { throw newException(args); } }
default void hasLength(@Nullable String text, Object... args) { if (!StringUtils.hasLength(text)) { throw newException(args); } }
default void doesNotContain(@Nullable String textToSearch, String substring, Object... args) { if (StringUtils.hasLength(textToSearch) && StringUtils.hasLength(substring) && textToSearch.contains(substring)) { throw newException(args); } }
default void hasText(@Nullable String text, Object... args) { if (!StringUtils.hasText(text)) { throw newException(args); } }
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); } }
default void isNull(@Nullable Object object, Object... args) { if (ObjectUtils.isNotEmpty(object)) { throw newException(args); } }
default void notNull(@Nullable Object object, Object... args) { if (ObjectUtils.isEmpty(object)) { throw newException(args); } }
default void isTrue(boolean expression, Object... args) { if (!expression) { throw newException(args); } }
default void noNullElements(@Nullable Object[] array, Object... args) { if (ObjectUtils.isNotEmpty(array)) { for (Object element : array) { if (ObjectUtils.isEmpty(element)) { throw newException(args); } } } }
default void noNullElements(@Nullable Collection<?> collection, Object... args) { if (!CollectionUtils.isEmpty(collection)) { for (Object element : collection) { if (ObjectUtils.isEmpty(element)) { throw newException(args); } } } }
default void notEmpty(@Nullable Object[] array, Object... args) { if (org.springframework.util.ObjectUtils.isEmpty(array)) { throw newException(args); } }
default void notEmpty(@Nullable Collection<?> collection, Object... args) { if (CollectionUtils.isEmpty(collection)) { throw newException(args); } }
default void notEmpty(@Nullable Map<?, ?> map, String... args) { if (CollectionUtils.isEmpty(map)) { throw newException(args); } }
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); }
static boolean endsWithSeparator(String msg) { return (msg.endsWith(":") || msg.endsWith(";") || msg.endsWith(",") || msg.endsWith(".")); }
static String messageWithTypeName(String msg, @Nullable Object typeName) { return msg + (msg.endsWith(" ") ? "" : ": ") + typeName; } }
|
继承 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
|
public interface GlobalExceptionAssert extends Assert, IResponseMsg {
@Override default GlobalException newException(Object... args) { return new GlobalException(this, args); }
@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
|
@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
|
@ControllerAdvice public class ExceptionTranslator {
@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
测试
可以发现结果如我们预期,接下来我们改变一下存在错误的情况(模拟第一个正常,第二个错误)
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
测试
可以发现结果依旧为我们预期的样子
结语
在本文中,我们深入探讨了 Java
中异常处理的多种方式以及如何使用断言来优化代码结构和提升开发效率。异常处理是软件开发中不可或缺的一部分,通过合理的异常处理机制,我们可以更好地保障程序的稳定性和可靠性。
我们首先介绍了 Java
中常见的异常处理方式,包括 try-catch
块、Throw
异常和自定义异常等。使用枚举类结合 Assert
,只需根据特定的异常情况定义不同的枚举实例,就能够针对不同情况抛出特定的异常,不用定义大量的异常类,同时还具备了断言的良好可读性。使用 断言 和 枚举类 相结合的方式,再配合统一异常处理,基本大部分的异常都能够被捕获。
希望本文对您理解和应用 Java
异常处理机制有所帮助。在实际项目中,根据具体需求选择合适的异常处理方式,是保证软件质量和开发效率的关键之一。愿您在今后的开发工作中能够运用这些技术,编写出更加健壮和可靠的 Java
程序!