Polish "Add failure analysis for missing auto-configured MailSender"

See gh-49582

Signed-off-by: Andy Wilkinson <andy.wilkinson@broadcom.com>
This commit is contained in:
Andy Wilkinson
2026-03-13 12:24:25 +00:00
parent ffb36cb5ed
commit f0aff3aa3c
2 changed files with 82 additions and 49 deletions
@@ -16,18 +16,27 @@
package org.springframework.boot.mail.autoconfigure;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport.ConditionAndOutcomes;
import org.springframework.boot.diagnostics.AbstractFailureAnalyzer;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.mail.autoconfigure.MailSenderAutoConfiguration.MailSenderCondition;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.mail.MailSender;
/**
* An {@link AbstractFailureAnalyzer} that improves missing {@link MailSender} guidance
* when mail auto-configuration is present but not activated.
* when {@link MailSenderAutoConfiguration} is present but did not match.
*
* @author MJY (answndud)
* @author Andy Wilkinson
*/
class NoSuchMailSenderBeanFailureAnalyzer extends AbstractFailureAnalyzer<NoSuchBeanDefinitionException>
implements Ordered {
@@ -36,18 +45,22 @@ class NoSuchMailSenderBeanFailureAnalyzer extends AbstractFailureAnalyzer<NoSuch
private static final String MAIL_JNDI_NAME_PROPERTY = "spring.mail.jndi-name";
private final @Nullable Environment environment;
private final BeanFactory beanFactory;
NoSuchMailSenderBeanFailureAnalyzer(@Nullable Environment environment) {
this.environment = environment;
NoSuchMailSenderBeanFailureAnalyzer(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
protected @Nullable FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionException cause) {
if (!isMissingMailSenderBean(cause) || hasMailConfigurationProperty()) {
if (!isMissingMailSenderBean(cause)) {
return null;
}
String description = "A MailSender bean could not be found because Spring Boot mail auto-configuration "
ConditionAndOutcome conditionAndOutcome = findMailSenderConditionOutcome();
if (conditionAndOutcome == null || conditionAndOutcome.getOutcome().isMatch()) {
return null;
}
String description = "A MailSender bean could not be found because MailSenderAutoConfiguration "
+ "did not match. Neither '" + MAIL_HOST_PROPERTY + "' nor '" + MAIL_JNDI_NAME_PROPERTY
+ "' is configured.";
String action = "Consider configuring '" + MAIL_HOST_PROPERTY + "' or '" + MAIL_JNDI_NAME_PROPERTY
@@ -56,6 +69,23 @@ class NoSuchMailSenderBeanFailureAnalyzer extends AbstractFailureAnalyzer<NoSuch
return new FailureAnalysis(description, action, cause);
}
private @Nullable ConditionAndOutcome findMailSenderConditionOutcome() {
ConditionEvaluationReport conditionEvaluationReport = ConditionEvaluationReport.find(this.beanFactory);
if (conditionEvaluationReport != null) {
Map<String, ConditionAndOutcomes> conditionAndOutcomesBySource = conditionEvaluationReport
.getConditionAndOutcomesBySource();
ConditionAndOutcomes conditionAndOutcomes = conditionAndOutcomesBySource
.get(MailSenderAutoConfiguration.class.getName());
if (conditionAndOutcomes != null) {
return conditionAndOutcomes.stream()
.filter((candidate) -> candidate.getCondition() instanceof MailSenderCondition)
.findFirst()
.orElse(null);
}
}
return null;
}
private boolean isMissingMailSenderBean(NoSuchBeanDefinitionException cause) {
Class<?> beanType = cause.getBeanType();
if (beanType == null && cause.getResolvableType() != null) {
@@ -64,11 +94,6 @@ class NoSuchMailSenderBeanFailureAnalyzer extends AbstractFailureAnalyzer<NoSuch
return (beanType != null) && MailSender.class.isAssignableFrom(beanType);
}
private boolean hasMailConfigurationProperty() {
return this.environment != null && (this.environment.containsProperty(MAIL_HOST_PROPERTY)
|| this.environment.containsProperty(MAIL_JNDI_NAME_PROPERTY));
}
@Override
public int getOrder() {
return 0;
@@ -18,65 +18,73 @@ package org.springframework.boot.mail.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.core.env.Environment;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.mail.MailSender;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
/**
* Tests for {@link NoSuchMailSenderBeanFailureAnalyzer}.
*
* @author MJY (answndud)
* @author Andy Wilkinson
*/
class NoSuchMailSenderBeanFailureAnalyzerTests {
@Test
void analyzeWhenNotNoSuchBeanDefinitionExceptionShouldReturnNull() {
assertThat(new NoSuchMailSenderBeanFailureAnalyzer(null).analyze(new Exception())).isNull();
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(MailSenderAutoConfiguration.class))
.run((context) -> {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
FailureAnalysis analysis = new NoSuchMailSenderBeanFailureAnalyzer(beanFactory)
.analyze(new Exception());
assertThat(analysis).isNull();
});
}
@Test
void analyzeWhenNoSuchBeanDefinitionExceptionForDifferentTypeShouldReturnNull() {
assertThat(
new NoSuchMailSenderBeanFailureAnalyzer(null).analyze(new NoSuchBeanDefinitionException(String.class)))
.isNull();
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(MailSenderAutoConfiguration.class))
.run((context) -> {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
assertThatException().isThrownBy(() -> context.getBean(String.class)).satisfies((ex) -> {
FailureAnalysis analysis = new NoSuchMailSenderBeanFailureAnalyzer(beanFactory).analyze(ex);
assertThat(analysis).isNull();
});
});
}
@Test
void analyzeWhenMailHostPropertyIsConfiguredShouldReturnNull() {
Environment environment = new MockEnvironment().withProperty("spring.mail.host", "smtp.example.org");
assertThat(new NoSuchMailSenderBeanFailureAnalyzer(environment)
.analyze(new NoSuchBeanDefinitionException(MailSender.class))).isNull();
void analyzeWithoutMailSenderAutoConfigurationShouldReturnNull() {
new ApplicationContextRunner().run((context) -> {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
assertThatException().isThrownBy(() -> context.getBean(MailSender.class)).satisfies((ex) -> {
FailureAnalysis analysis = new NoSuchMailSenderBeanFailureAnalyzer(beanFactory).analyze(ex);
assertThat(analysis).isNull();
});
});
}
@Test
void analyzeWhenMailJndiNamePropertyIsConfiguredShouldReturnNull() {
Environment environment = new MockEnvironment().withProperty("spring.mail.jndi-name", "mail/Session");
assertThat(new NoSuchMailSenderBeanFailureAnalyzer(environment)
.analyze(new NoSuchBeanDefinitionException(MailSender.class))).isNull();
}
@Test
void analyzeWhenMailSenderBeanIsMissingAndNoMailPropertiesAreConfiguredShouldProvideGuidance() {
FailureAnalysis analysis = new NoSuchMailSenderBeanFailureAnalyzer(new MockEnvironment())
.analyze(new NoSuchBeanDefinitionException(MailSender.class));
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription())
.contains("A MailSender bean could not be found")
.contains("spring.mail.host")
.contains("spring.mail.jndi-name");
assertThat(analysis.getAction())
.contains("spring.mail.host")
.contains("spring.mail.jndi-name")
.contains("MailSender bean");
}
@Test
void analyzeWhenJavaMailSenderBeanIsMissingAndNoMailPropertiesAreConfiguredShouldProvideGuidance() {
assertThat(new NoSuchMailSenderBeanFailureAnalyzer(new MockEnvironment())
.analyze(new NoSuchBeanDefinitionException(JavaMailSender.class))).isNotNull();
void analyzeWhenMailSenderBeanIsMissingAndMailSenderConditionDidNotMatchShouldProvideGuidance() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(MailSenderAutoConfiguration.class))
.run((context) -> {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
assertThatException().isThrownBy(() -> context.getBean(MailSender.class)).satisfies((ex) -> {
FailureAnalysis analysis = new NoSuchMailSenderBeanFailureAnalyzer(beanFactory).analyze(ex);
assertThat(analysis).isNotNull();
assertThat(analysis.getDescription()).contains("A MailSender bean could not be found")
.contains("spring.mail.host")
.contains("spring.mail.jndi-name");
assertThat(analysis.getAction()).contains("spring.mail.host")
.contains("spring.mail.jndi-name")
.contains("MailSender bean");
});
});
}
}