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..7997fc332c8 --- /dev/null +++ b/module/spring-boot-mail/src/main/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzer.java @@ -0,0 +1,102 @@ +/* + * 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 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.mail.MailSender; + +/** + * An {@link AbstractFailureAnalyzer} that improves missing {@link MailSender} guidance + * when {@link MailSenderAutoConfiguration} is present but did not match. + * + * @author MJY (answndud) + * @author Andy Wilkinson + */ +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 BeanFactory beanFactory; + + NoSuchMailSenderBeanFailureAnalyzer(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + protected @Nullable FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionException cause) { + if (!isMissingMailSenderBean(cause)) { + return null; + } + 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 + + "' 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 @Nullable ConditionAndOutcome findMailSenderConditionOutcome() { + ConditionEvaluationReport conditionEvaluationReport = ConditionEvaluationReport.find(this.beanFactory); + if (conditionEvaluationReport != null) { + Map 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) { + beanType = cause.getResolvableType().resolve(); + } + return (beanType != null) && MailSender.class.isAssignableFrom(beanType); + } + + @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..7c719ac1a47 --- /dev/null +++ b/module/spring-boot-mail/src/test/java/org/springframework/boot/mail/autoconfigure/NoSuchMailSenderBeanFailureAnalyzerTests.java @@ -0,0 +1,90 @@ +/* + * 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.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.mail.MailSender; + +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() { + 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() { + 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 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 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"); + }); + }); + } + +}