From ffb36cb5ed7eaf6b24306a897cd228906a2c8308 Mon Sep 17 00:00:00 2001 From: MJY Date: Fri, 13 Mar 2026 17:20:55 +0900 Subject: [PATCH 1/2] Add failure analysis for missing auto-configured MailSender Signed-off-by: MJY See gh-49582 --- .../NoSuchMailSenderBeanFailureAnalyzer.java | 77 +++++++++++++++++ .../main/resources/META-INF/spring.factories | 3 + ...uchMailSenderBeanFailureAnalyzerTests.java | 82 +++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzer.java create mode 100644 module/spring-boot-mail/src/main/resources/META-INF/spring.factories create mode 100644 module/spring-boot-mail/src/test/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzerTests.java diff --git a/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzer.java b/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzer.java new file mode 100644 index 00000000000..985e9daf8db --- /dev/null +++ b/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzer.java @@ -0,0 +1,77 @@ +/* + * 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.mail.autoconfigure; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +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. + */ +class NoSuchMailSenderBeanFailureAnalyzer extends AbstractFailureAnalyzer + implements Ordered { + + private static final String MAIL_HOST_PROPERTY = "spring.mail.host"; + + private static final String MAIL_JNDI_NAME_PROPERTY = "spring.mail.jndi-name"; + + private final @Nullable Environment environment; + + NoSuchMailSenderBeanFailureAnalyzer(@Nullable Environment environment) { + this.environment = environment; + } + + @Override + protected @Nullable FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionException cause) { + if (!isMissingMailSenderBean(cause) || hasMailConfigurationProperty()) { + return null; + } + String description = "A MailSender bean could not be found because Spring Boot mail auto-configuration " + + "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 + + "' to enable auto-configuration. If you want to use a custom mail sender, define a MailSender " + + "bean in your configuration."; + return new FailureAnalysis(description, action, cause); + } + + private boolean isMissingMailSenderBean(NoSuchBeanDefinitionException cause) { + Class beanType = cause.getBeanType(); + if (beanType == null && cause.getResolvableType() != null) { + beanType = cause.getResolvableType().resolve(); + } + 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; + } + +} diff --git a/module/spring-boot-mail/src/main/resources/META-INF/spring.factories b/module/spring-boot-mail/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..bfd0e1dd108 --- /dev/null +++ b/module/spring-boot-mail/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Failure Analyzers +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.mail.autoconfigure.NoSuchMailSenderBeanFailureAnalyzer diff --git a/module/spring-boot-mail/src/test/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzerTests.java b/module/spring-boot-mail/src/test/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzerTests.java new file mode 100644 index 00000000000..b3b03121fb1 --- /dev/null +++ b/module/spring-boot-mail/src/test/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzerTests.java @@ -0,0 +1,82 @@ +/* + * 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.mail.autoconfigure; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.core.env.Environment; +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; + +/** + * Tests for {@link NoSuchMailSenderBeanFailureAnalyzer}. + */ +class NoSuchMailSenderBeanFailureAnalyzerTests { + + @Test + void analyzeWhenNotNoSuchBeanDefinitionExceptionShouldReturnNull() { + assertThat(new NoSuchMailSenderBeanFailureAnalyzer(null).analyze(new Exception())).isNull(); + } + + @Test + void analyzeWhenNoSuchBeanDefinitionExceptionForDifferentTypeShouldReturnNull() { + assertThat( + new NoSuchMailSenderBeanFailureAnalyzer(null).analyze(new NoSuchBeanDefinitionException(String.class))) + .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(); + } + + @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(); + } + +} From f0aff3aa3c5ef7c51549823b5b8afba9a159f8db Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 13 Mar 2026 12:02:32 +0000 Subject: [PATCH 2/2] Polish "Add failure analysis for missing auto-configured MailSender" See gh-49582 Signed-off-by: Andy Wilkinson --- .../NoSuchMailSenderBeanFailureAnalyzer.java | 49 ++++++++--- ...uchMailSenderBeanFailureAnalyzerTests.java | 82 ++++++++++--------- 2 files changed, 82 insertions(+), 49 deletions(-) diff --git a/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzer.java b/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzer.java index 985e9daf8db..7997fc332c8 100644 --- a/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzer.java +++ b/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzer.java @@ -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 implements Ordered { @@ -36,18 +45,22 @@ class NoSuchMailSenderBeanFailureAnalyzer extends AbstractFailureAnalyzer 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 { + 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"); + }); + }); } }