Add FailureAnalyzedException support

Add `FailureAnalyzedException` to reduce the number of internal
`FailureAnalizers` required when only a simple "description" and
"action" are required.

Closes gh-49320
This commit is contained in:
Phillip Webb
2026-02-24 22:39:00 -08:00
parent 11825b9b17
commit 2182339bb1
3 changed files with 125 additions and 2 deletions
@@ -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;
}
}
@@ -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<FailureAnalyzer> loadFailureAnalyzers(@Nullable ConfigurableApplicationContext context,
SpringFactoriesLoader springFactoriesLoader) {
return springFactoriesLoader.load(FailureAnalyzer.class, getArgumentResolver(context),
FailureHandler.logging(logger));
List<FailureAnalyzer> 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) {
@@ -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<FailureAnalysis> 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<? extends FailureAnalyzer>... failureAnalyzerClasses) {
analyzeAndReport(failure, this.context, failureAnalyzerClasses);
@@ -99,6 +135,7 @@ class FailureAnalyzersTests {
for (Class<? extends FailureAnalyzer> 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;
}
}
}