Skip to content

Commit ce7d384

Browse files
committed
Add MissingParametersFailureAnalyzer
Add a new failure analyzer that provides hints whenever parameter names cannot be discovered. Closes gh-38603
1 parent f609022 commit ce7d384

File tree

6 files changed

+353
-8
lines changed

6 files changed

+353
-8
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzer.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,20 @@ protected FailureAnalysis analyze(Throwable rootFailure, BindException cause) {
4949
|| rootCause instanceof UnboundConfigurationPropertiesException) {
5050
return null;
5151
}
52-
return analyzeGenericBindException(cause);
52+
return analyzeGenericBindException(rootFailure, cause);
5353
}
5454

55-
private FailureAnalysis analyzeGenericBindException(BindException cause) {
55+
private FailureAnalysis analyzeGenericBindException(Throwable rootFailure, BindException cause) {
56+
FailureAnalysis missingParametersAnalysis = MissingParameterNamesFailureAnalyzer
57+
.analyzeForMissingParameters(rootFailure);
5658
StringBuilder description = new StringBuilder(String.format("%s:%n", cause.getMessage()));
5759
ConfigurationProperty property = cause.getProperty();
5860
buildDescription(description, property);
5961
description.append(String.format("%n Reason: %s", getMessage(cause)));
60-
return getFailureAnalysis(description, cause);
62+
if (missingParametersAnalysis != null) {
63+
MissingParameterNamesFailureAnalyzer.appendPossibility(description);
64+
}
65+
return getFailureAnalysis(description.toString(), cause, missingParametersAnalysis);
6166
}
6267

6368
private void buildDescription(StringBuilder description, ConfigurationProperty property) {
@@ -98,14 +103,18 @@ private String getExceptionTypeAndMessage(Throwable ex) {
98103
return ex.getClass().getName() + (StringUtils.hasText(message) ? ": " + message : "");
99104
}
100105

101-
private FailureAnalysis getFailureAnalysis(Object description, BindException cause) {
102-
StringBuilder message = new StringBuilder("Update your application's configuration");
106+
private FailureAnalysis getFailureAnalysis(String description, BindException cause,
107+
FailureAnalysis missingParametersAnalysis) {
108+
StringBuilder action = new StringBuilder("Update your application's configuration");
103109
Collection<String> validValues = findValidValues(cause);
104110
if (!validValues.isEmpty()) {
105-
message.append(String.format(". The following values are valid:%n"));
106-
validValues.forEach((value) -> message.append(String.format("%n %s", value)));
111+
action.append(String.format(". The following values are valid:%n"));
112+
validValues.forEach((value) -> action.append(String.format("%n %s", value)));
113+
}
114+
if (missingParametersAnalysis != null) {
115+
action.append(String.format("%n%n%s", missingParametersAnalysis.getAction()));
107116
}
108-
return new FailureAnalysis(description.toString(), message.toString(), cause);
117+
return new FailureAnalysis(description, action.toString(), cause);
109118
}
110119

111120
private Collection<String> findValidValues(BindException ex) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.diagnostics.analyzer;
18+
19+
import java.util.HashSet;
20+
import java.util.Set;
21+
22+
import org.springframework.boot.diagnostics.FailureAnalysis;
23+
import org.springframework.boot.diagnostics.FailureAnalyzer;
24+
import org.springframework.core.Ordered;
25+
import org.springframework.core.annotation.Order;
26+
import org.springframework.util.StringUtils;
27+
28+
/**
29+
* {@link FailureAnalyzer} for exceptions caused by missing parameter names. This analyzer
30+
* is ordered last, if other analyzers wish to also report parameter actions they can use
31+
* the {@link #analyzeForMissingParameters(Throwable)} static method.
32+
*
33+
* @author Phillip Webb
34+
*/
35+
@Order(Ordered.LOWEST_PRECEDENCE)
36+
class MissingParameterNamesFailureAnalyzer implements FailureAnalyzer {
37+
38+
private static final String USE_PARAMETERS_MESSAGE = "Ensure that the compiler uses the '-parameters' flag";
39+
40+
static final String POSSIBILITY = "This may be due to missing parameter name information";
41+
42+
static String ACTION = """
43+
Ensure that your compiler is configured to use the '-parameters' flag.
44+
You may need to update both your build tool settings as well as your IDE.
45+
(See https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x#parameter-name-retention)
46+
""";
47+
48+
@Override
49+
public FailureAnalysis analyze(Throwable failure) {
50+
return analyzeForMissingParameters(failure);
51+
}
52+
53+
/**
54+
* Analyze the given failure for missing parameter name exceptions.
55+
* @param failure the failure to analyze
56+
* @return a failure analysis or {@code null}
57+
*/
58+
static FailureAnalysis analyzeForMissingParameters(Throwable failure) {
59+
return analyzeForMissingParameters(failure, failure, new HashSet<>());
60+
}
61+
62+
private static FailureAnalysis analyzeForMissingParameters(Throwable rootFailure, Throwable cause,
63+
Set<Throwable> seen) {
64+
if (cause != null && seen.add(cause)) {
65+
if (isSpringParametersException(cause)) {
66+
return getAnalysis(rootFailure, cause);
67+
}
68+
FailureAnalysis analysis = analyzeForMissingParameters(rootFailure, cause.getCause(), seen);
69+
if (analysis != null) {
70+
return analysis;
71+
}
72+
for (Throwable suppressed : cause.getSuppressed()) {
73+
analysis = analyzeForMissingParameters(rootFailure, suppressed, seen);
74+
if (analysis != null) {
75+
return analysis;
76+
}
77+
}
78+
}
79+
return null;
80+
}
81+
82+
private static boolean isSpringParametersException(Throwable failure) {
83+
String message = failure.getMessage();
84+
return message != null && message.contains(USE_PARAMETERS_MESSAGE) && isSpringException(failure);
85+
}
86+
87+
private static boolean isSpringException(Throwable failure) {
88+
StackTraceElement[] elements = failure.getStackTrace();
89+
return elements.length > 0 && isSpringClass(elements[0].getClassName());
90+
}
91+
92+
private static boolean isSpringClass(String className) {
93+
return className != null && className.startsWith("org.springframework.");
94+
}
95+
96+
private static FailureAnalysis getAnalysis(Throwable rootFailure, Throwable cause) {
97+
StringBuilder description = new StringBuilder(String.format("%s:%n", cause.getMessage()));
98+
if (rootFailure != cause) {
99+
description.append(String.format("%n Resulting Failure: %s", getExceptionTypeAndMessage(rootFailure)));
100+
}
101+
return new FailureAnalysis(description.toString(), ACTION, rootFailure);
102+
}
103+
104+
private static String getExceptionTypeAndMessage(Throwable ex) {
105+
String message = ex.getMessage();
106+
return ex.getClass().getName() + (StringUtils.hasText(message) ? ": " + message : "");
107+
}
108+
109+
public static void appendPossibility(StringBuilder description) {
110+
if (!description.toString().endsWith(System.lineSeparator())) {
111+
description.append("%n".formatted());
112+
}
113+
description.append("%n%s".formatted(POSSIBILITY));
114+
}
115+
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.diagnostics.analyzer;
18+
19+
import java.util.HashSet;
20+
import java.util.Set;
21+
22+
import org.springframework.boot.diagnostics.FailureAnalysis;
23+
import org.springframework.boot.diagnostics.FailureAnalyzer;
24+
import org.springframework.core.Ordered;
25+
import org.springframework.core.annotation.Order;
26+
27+
/**
28+
* @author Phillip Webb
29+
*/
30+
@Order(Ordered.HIGHEST_PRECEDENCE)
31+
class MissingParametersFailureAnalyzer implements FailureAnalyzer {
32+
33+
private static final String USE_PARAMETERS_MESSAGE = "Ensure that the compiler uses the '-parameters' flag";
34+
35+
@Override
36+
public FailureAnalysis analyze(Throwable failure) {
37+
return analyze(failure, failure, new HashSet<>());
38+
}
39+
40+
private FailureAnalysis analyze(Throwable rootFailure, Throwable cause, Set<Throwable> seen) {
41+
if (cause == null || !seen.add(cause)) {
42+
return null;
43+
}
44+
if (isSpringParametersException(cause)) {
45+
return getAnalysis(rootFailure, cause);
46+
}
47+
FailureAnalysis analysis = analyze(rootFailure, cause.getCause(), seen);
48+
if (analysis != null) {
49+
return analysis;
50+
}
51+
for (Throwable suppressed : cause.getSuppressed()) {
52+
analysis = analyze(rootFailure, suppressed, seen);
53+
if (analysis != null) {
54+
return analysis;
55+
}
56+
}
57+
return null;
58+
}
59+
60+
private boolean isSpringParametersException(Throwable failure) {
61+
String message = failure.getMessage();
62+
return message != null && message.contains(USE_PARAMETERS_MESSAGE) && isSpringException(failure);
63+
}
64+
65+
private boolean isSpringException(Throwable failure) {
66+
StackTraceElement[] elements = failure.getStackTrace();
67+
return elements.length > 0 && isSpringClass(elements[0].getClassName());
68+
}
69+
70+
private boolean isSpringClass(String className) {
71+
return className != null && className.startsWith("org.springframework.");
72+
}
73+
74+
private FailureAnalysis getAnalysis(Throwable rootFailure, Throwable cause) {
75+
String description = "";
76+
if (rootFailure != cause) {
77+
78+
}
79+
String action = "";
80+
return new FailureAnalysis(description, action, rootFailure);
81+
}
82+
83+
}

spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyz
7070
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
7171
org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\
7272
org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\
73+
org.springframework.boot.diagnostics.analyzer.MissingParameterNamesFailureAnalyzer,\
7374
org.springframework.boot.diagnostics.analyzer.MutuallyExclusiveConfigurationPropertiesFailureAnalyzer,\
7475
org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\
7576
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/diagnostics/analyzer/BindFailureAnalyzerTests.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,19 @@ void bindExceptionDueToMapConversionFailure() {
9999
+ "org.springframework.boot.logging.LogLevel>]"));
100100
}
101101

102+
@Test
103+
void bindExceptionWithSupressedMissingParametersException() {
104+
BeanCreationException failure = createFailure(GenericFailureConfiguration.class, "test.foo.value=alpha");
105+
failure.addSuppressed(new IllegalStateException(
106+
"Missing parameter names. Ensure that the compiler uses the '-parameters' flag"));
107+
FailureAnalysis analysis = new BindFailureAnalyzer().analyze(failure);
108+
assertThat(analysis.getDescription())
109+
.contains(failure("test.foo.value", "alpha", "\"test.foo.value\" from property source \"test\"",
110+
"failed to convert java.lang.String to int"))
111+
.contains(MissingParameterNamesFailureAnalyzer.POSSIBILITY);
112+
assertThat(analysis.getAction()).contains(MissingParameterNamesFailureAnalyzer.ACTION);
113+
}
114+
102115
private static String failure(String property, String value, String origin, String reason) {
103116
return String.format("Property: %s%n Value: \"%s\"%n Origin: %s%n Reason: %s", property, value, origin,
104117
reason);

0 commit comments

Comments
 (0)