diff --git a/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/ExceptionUtils.java b/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/ExceptionUtils.java index d6ca6144..da2b6d00 100644 --- a/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/ExceptionUtils.java +++ b/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/ExceptionUtils.java @@ -55,7 +55,7 @@ public static Throwable getActualCause(MethodInvocationException exception) { /** * 获取异常的原因。 * - * @param throwable 表示指定异常的 {@link Exception}。 + * @param throwable 表示指定异常的 {@link Throwable}。 * @return 表示异常原因的 {@link String}。 */ public static String getReason(Throwable throwable) { @@ -64,4 +64,23 @@ public static String getReason(Throwable throwable) { } return throwable.getClass().getSimpleName() + ": " + throwable.getMessage(); } + + /** + * 获取从异常链中找出第一个非空的异常消息。 + * + * @param throwable 表示指定异常的 {@link Throwable}。 + * @return 表示异常原因的 {@link String}。 + */ + public static String getActualMessage(Throwable throwable) { + Set visited = new HashSet<>(); + while (throwable != null && !visited.contains(throwable)) { + visited.add(throwable); + String message = throwable.getMessage(); + if (StringUtils.isNotBlank(message)) { + return message; + } + throwable = throwable.getCause(); + } + return null; + } } diff --git a/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/ExceptionUtilsTest.java b/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/ExceptionUtilsTest.java index 98c49c20..f9126132 100644 --- a/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/ExceptionUtilsTest.java +++ b/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/ExceptionUtilsTest.java @@ -106,4 +106,43 @@ void givenExceptionCauseHasAnotherTypeCauseThenGetItsDescendantsCause() { assertThat(actualCause).isNotNull().hasMessage("error"); } } + + @Nested + @DisplayName("test getActualMessage(Throwable throwable)") + class WhenGetActualMessage { + @Test + @DisplayName("when throwable has non-blank message, return the message") + void givenThrowableWithMessageThenReturnMessage() { + Throwable throwable = new Exception("Error occurred"); + String message = ExceptionUtils.getActualMessage(throwable); + assertThat(message).isEqualTo("Error occurred"); + } + + @Test + @DisplayName("when throwable chain has non-blank message in deep cause, return the first valid message") + void givenDeepThrowableChainThenReturnFirstValidMessage() { + Throwable cause = new Exception("Root cause"); + Throwable throwable = new Exception(null, new Exception(" ", cause)); + String message = ExceptionUtils.getActualMessage(throwable); + assertThat(message).isEqualTo("Root cause"); + } + + @Test + @DisplayName("when throwable chain has all blank messages, return null") + void givenAllBlankMessagesThenReturnNull() { + Throwable throwable = new Exception(null, new Exception(" ")); + String message = ExceptionUtils.getActualMessage(throwable); + assertThat(message).isNull(); + } + + @Test + @DisplayName("when throwable chain has circular reference, return valid message") + void givenCircularReferenceThenReturnFirstValidMessage() { + Exception e1 = new Exception("e1"); + Exception e2 = new Exception("e2", e1); + e1.initCause(e2); + assertThat(ExceptionUtils.getActualMessage(e1)).isEqualTo("e1"); + assertThat(ExceptionUtils.getActualMessage(e2)).isEqualTo("e2"); + } + } }