fix(notifications): EmailChannel @Bean registration вместо @Component (race fix)
This commit is contained in:
+14
-6
@@ -8,12 +8,9 @@ import jakarta.mail.MessagingException;
|
|||||||
import jakarta.mail.internet.MimeMessage;
|
import jakarta.mail.internet.MimeMessage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.MailException;
|
||||||
import org.springframework.mail.javamail.JavaMailSender;
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -36,14 +33,25 @@ import java.time.Duration;
|
|||||||
* "tried, gave up", не долбим SMTP бесконечно).</li>
|
* "tried, gave up", не долбим SMTP бесконечно).</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@Component
|
|
||||||
@ConditionalOnProperty(name = "ordinis.notifications.enabled", havingValue = "true", matchIfMissing = false)
|
|
||||||
// Без JavaMailSender bean (т.е. spring.mail.host не задан) ChannelKind.EMAIL
|
// Без JavaMailSender bean (т.е. spring.mail.host не задан) ChannelKind.EMAIL
|
||||||
// не активируется. Это позволяет включить notifications.enabled=true для
|
// не активируется. Это позволяет включить notifications.enabled=true для
|
||||||
// Express-only deploy без обязательной SMTP-конфигурации, и не вешает Spring
|
// Express-only deploy без обязательной SMTP-конфигурации, и не вешает Spring
|
||||||
// boot на UnsatisfiedDependencyException когда mail starter в classpath, но
|
// 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 {
|
public class EmailChannel implements NotificationChannel {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(EmailChannel.class);
|
private static final Logger log = LoggerFactory.getLogger(EmailChannel.class);
|
||||||
|
|||||||
+31
@@ -1,6 +1,11 @@
|
|||||||
package cloud.nstart.terravault.ordinis.notifications.config;
|
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.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.cache.annotation.EnableCaching;
|
import org.springframework.cache.annotation.EnableCaching;
|
||||||
import org.springframework.context.MessageSource;
|
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.ComponentScan;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
|
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -29,6 +35,11 @@ import java.nio.charset.StandardCharsets;
|
|||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
@EnableCaching
|
@EnableCaching
|
||||||
@ComponentScan(basePackages = "cloud.nstart.terravault.ordinis.notifications")
|
@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 {
|
public class NotificationsAutoConfiguration {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,4 +57,24 @@ public class NotificationsAutoConfiguration {
|
|||||||
ms.setUseCodeAsDefaultMessage(false); // throw NoSuchMessageException — fail loud
|
ms.setUseCodeAsDefaultMessage(false); // throw NoSuchMessageException — fail loud
|
||||||
return ms;
|
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: []".
|
||||||
|
*
|
||||||
|
* <p>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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user