diff --git a/ordinis-notifications/src/main/java/cloud/nstart/terravault/ordinis/notifications/channel/EmailChannel.java b/ordinis-notifications/src/main/java/cloud/nstart/terravault/ordinis/notifications/channel/EmailChannel.java index 880d29b..1a1995f 100644 --- a/ordinis-notifications/src/main/java/cloud/nstart/terravault/ordinis/notifications/channel/EmailChannel.java +++ b/ordinis-notifications/src/main/java/cloud/nstart/terravault/ordinis/notifications/channel/EmailChannel.java @@ -8,12 +8,9 @@ import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.mail.MailException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; -import org.springframework.stereotype.Component; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -36,14 +33,25 @@ import java.time.Duration; * "tried, gave up", не долбим SMTP бесконечно). * */ -@Component -@ConditionalOnProperty(name = "ordinis.notifications.enabled", havingValue = "true", matchIfMissing = false) // Без JavaMailSender bean (т.е. spring.mail.host не задан) ChannelKind.EMAIL // не активируется. Это позволяет включить notifications.enabled=true для // Express-only deploy без обязательной SMTP-конфигурации, и не вешает Spring // boot на UnsatisfiedDependencyException когда mail starter в classpath, но // инфра почты не подведена. -@ConditionalOnBean(JavaMailSender.class) +// +// HISTORY: раньше класс был @Component + @ConditionalOnBean(JavaMailSender) +// на class-level. Это classic Spring race ordering bug — @Component scanned +// через scanBasePackages ДО auto-config phase, на момент evaluation +// JavaMailSender bean не создан → @ConditionalOnBean fails → EmailChannel +// не register'ится. MailSenderPropertiesConfiguration создаёт JavaMailSender +// позже, но EmailChannel уже отброшен. Spring docs прямо warning: +// "It is generally safe to use @ConditionalOnBean and @ConditionalOnMissingBean +// at the @Bean method level only". +// +// Fix: @Component удалён, instance создаётся как @Bean в +// NotificationsAutoConfiguration (которая @AutoConfigureAfter MailSenderAutoConfiguration). +// На момент @Bean-метода JavaMailSender уже существует → @ConditionalOnBean +// matches → EmailChannel registered. public class EmailChannel implements NotificationChannel { private static final Logger log = LoggerFactory.getLogger(EmailChannel.class); diff --git a/ordinis-notifications/src/main/java/cloud/nstart/terravault/ordinis/notifications/config/NotificationsAutoConfiguration.java b/ordinis-notifications/src/main/java/cloud/nstart/terravault/ordinis/notifications/config/NotificationsAutoConfiguration.java index 6d54931..2bb0523 100644 --- a/ordinis-notifications/src/main/java/cloud/nstart/terravault/ordinis/notifications/config/NotificationsAutoConfiguration.java +++ b/ordinis-notifications/src/main/java/cloud/nstart/terravault/ordinis/notifications/config/NotificationsAutoConfiguration.java @@ -1,6 +1,11 @@ package cloud.nstart.terravault.ordinis.notifications.config; +import cloud.nstart.terravault.ordinis.notifications.channel.EmailChannel; +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.MessageSource; @@ -8,6 +13,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.mail.javamail.JavaMailSender; import org.springframework.scheduling.annotation.EnableScheduling; import java.nio.charset.StandardCharsets; @@ -29,6 +35,11 @@ import java.nio.charset.StandardCharsets; @EnableScheduling @EnableCaching @ComponentScan(basePackages = "cloud.nstart.terravault.ordinis.notifications") +// @AutoConfigureAfter гарантирует что MailSenderAutoConfiguration evaluated +// раньше — на момент создания emailChannel() bean'а JavaMailSender уже +// существует в context'е. Без этого @ConditionalOnBean(JavaMailSender.class) +// fails (race ordering — см. comment в EmailChannel.java HISTORY). +@AutoConfigureAfter(MailSenderAutoConfiguration.class) public class NotificationsAutoConfiguration { /** @@ -46,4 +57,24 @@ public class NotificationsAutoConfiguration { ms.setUseCodeAsDefaultMessage(false); // throw NoSuchMessageException — fail loud return ms; } + + /** + * EmailChannel registered как @Bean (НЕ @Component) — позволяет + * @ConditionalOnBean(JavaMailSender.class) evaluation на bean-method level + * после того как MailSenderAutoConfiguration отработал. Раньше EmailChannel + * был @Component с class-level @ConditionalOnBean → race с component scan + * → JavaMailSender ещё не существует на момент check → bean не register'ится + * → NotificationsDispatcher logs WARN "Channels: []". + * + *
Spring docs: "It is generally safe to use @ConditionalOnBean and + * @ConditionalOnMissingBean at the @Bean method level only". + */ + @Bean + @ConditionalOnBean(JavaMailSender.class) + public EmailChannel emailChannel( + JavaMailSender mailSender, + NotificationsProperties props, + MeterRegistry meterRegistry) { + return new EmailChannel(mailSender, props, meterRegistry); + } }