diff --git a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java new file mode 100644 index 00000000000..d4110f7e804 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.diagnostics; + +import org.jspecify.annotations.Nullable; + +/** + * {@link RuntimeException} that includes a {@link FailureAnalysis}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +public class FailureAnalyzedException extends RuntimeException { + + private final @Nullable String action; + + /** + * Create a new {@link FailureAnalyzedException} instance. + * @param description the {@link FailureAnalysis} description + * @param action the {@link FailureAnalysis} action + */ + public FailureAnalyzedException(String description, @Nullable String action) { + super(description); + this.action = action; + } + + /** + * Create a new {@link FailureAnalyzedException} instance. + * @param description the {@link FailureAnalysis} description + * @param action the {@link FailureAnalysis} action + * @param cause the cause of the exception + */ + public FailureAnalyzedException(String description, @Nullable String action, Throwable cause) { + super(description, cause); + this.action = action; + } + + /** + * Return the {@link FailureAnalysis} to use for this exception. + * @return the failure analysis + */ + public FailureAnalysis analysis() { + return new FailureAnalysis(getMessage(), this.action, this); + } + + static @Nullable FailureAnalysis analyze(Throwable failure) { + while (failure != null) { + if (failure instanceof FailureAnalyzedException failureAnalyzedException) { + return failureAnalyzedException.analysis(); + } + failure = failure.getCause(); + } + return null; + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java index 6c967fa3a48..c56f2129b7b 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java @@ -16,6 +16,8 @@ package org.springframework.boot.diagnostics; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.apache.commons.logging.Log; @@ -64,8 +66,10 @@ final class FailureAnalyzers implements SpringBootExceptionReporter { private static List loadFailureAnalyzers(@Nullable ConfigurableApplicationContext context, SpringFactoriesLoader springFactoriesLoader) { - return springFactoriesLoader.load(FailureAnalyzer.class, getArgumentResolver(context), - FailureHandler.logging(logger)); + List analyzers = new ArrayList<>(springFactoriesLoader.load(FailureAnalyzer.class, + getArgumentResolver(context), FailureHandler.logging(logger))); + analyzers.add(FailureAnalyzedException::analyze); + return Collections.unmodifiableList(analyzers); } private static @Nullable ArgumentResolver getArgumentResolver(@Nullable ConfigurableApplicationContext context) { diff --git a/core/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java b/core/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java index b2ab039bc1f..13c0b6ad52c 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/diagnostics/FailureAnalyzersTests.java @@ -16,6 +16,9 @@ package org.springframework.boot.diagnostics; +import java.util.ArrayList; +import java.util.List; + import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -47,6 +50,8 @@ class FailureAnalyzersTests { private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + private final List reports = new ArrayList<>(); + @BeforeEach void configureMock() { failureAnalyzer = mock(FailureAnalyzer.class); @@ -87,6 +92,37 @@ class FailureAnalyzersTests { then(failureAnalyzer).should().analyze(failure); } + @Test + void failureAnalyzedExceptionAreReported() { + FailureAnalyzedException failure = new FailureAnalyzedException("Bad", "Fix it!"); + analyzeAndReport(failure); + assertThat(this.reports).hasSize(1); + FailureAnalysis report = this.reports.get(0); + assertThat(report.getDescription()).isEqualTo("Bad"); + assertThat(report.getAction()).isEqualTo("Fix it!"); + assertThat(report.getCause()).isSameAs(failure); + } + + @Test + void wrappedFailureAnalyzedExceptionAreReported() { + FailureAnalyzedException failure = new FailureAnalyzedException("Bad", "Fix it!"); + analyzeAndReport(new IllegalStateException("the state of this!", failure)); + assertThat(this.reports).hasSize(1); + FailureAnalysis report = this.reports.get(0); + assertThat(report.getDescription()).isEqualTo("Bad"); + assertThat(report.getAction()).isEqualTo("Fix it!"); + assertThat(report.getCause()).isSameAs(failure); + } + + @Test + void whenOtherAnalyzerIsAvailableFailureAnalyzedExceptionAreNotReported() { + FailureAnalyzedException failure = new FailureAnalyzedException("Bad", "Fix it!"); + analyzeAndReport(new IllegalStateException("the state of this!", failure), IllegalStateFailureAnalyzer.class); + assertThat(this.reports).hasSize(1); + FailureAnalysis report = this.reports.get(0); + assertThat(report.getDescription()).isEqualTo("analyzed state"); + } + @SafeVarargs private void analyzeAndReport(Throwable failure, Class... failureAnalyzerClasses) { analyzeAndReport(failure, this.context, failureAnalyzerClasses); @@ -99,6 +135,7 @@ class FailureAnalyzersTests { for (Class failureAnalyzerClass : failureAnalyzerClasses) { loader.add(FailureAnalyzer.class, failureAnalyzerClass); } + loader.addInstance(FailureAnalysisReporter.class, this.reports::add); new FailureAnalyzers(context, loader).reportException(failure); } @@ -151,4 +188,16 @@ class FailureAnalyzersTests { } + static class IllegalStateFailureAnalyzer implements FailureAnalyzer { + + @Override + public @Nullable FailureAnalysis analyze(Throwable failure) { + if (failure instanceof IllegalStateException) { + return new FailureAnalysis("analyzed state", null, failure); + } + return null; + } + + } + }