Через что отправляли уведомления
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Системы отправки уведомлений в Java приложениях
В проектах я использовал разные подходы в зависимости от требований. Расскажу о наиболее практичных решениях, которые работают в боевых условиях.
1. Email уведомления
Через SMTP (классический способ)
// Maven dependency
// <dependency>
// <groupId>org.springframework.boot</groupId>
// <artifactId>spring-boot-starter-mail</artifactId>
// </dependency>
@Service
public class EmailNotificationService {
@Autowired
private JavaMailSender mailSender;
public void sendWelcomeEmail(String email, String userName) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("noreply@company.com");
message.setTo(email);
message.setSubject("Добро пожаловать!");
message.setText("Привет, " + userName + "!");
mailSender.send(message);
}
}
Проблемы базового подхода:
- SMTP медленный (может быть 1-2 сек на письмо)
- Блокирует основной поток
- Может упасть, если почтовый сервер недоступен
Решение: асинхронная отправка
@Service
public class EmailNotificationService {
@Autowired
private JavaMailSender mailSender;
@Async // Выполняется в отдельном потоке
public void sendWelcomeEmail(String email, String userName) {
try {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("noreply@company.com");
message.setTo(email);
message.setSubject("Добро пожаловать!");
message.setText("Привет, " + userName + "!");
mailSender.send(message);
} catch (MailException e) {
log.error("Ошибка отправки письма: " + email, e);
// Логируешь ошибку для retry
}
}
}
С использованием сервиса отправки (рекомендуется)
// SendGrid, Mailgun, AWS SES, Twilio
@Service
public class EmailNotificationService {
@Value("${sendgrid.api-key}")
private String sendGridApiKey;
public void sendWelcomeEmail(String email, String userName) {
Email from = new Email("noreply@company.com");
String subject = "Добро пожаловать!";
Email to = new Email(email);
Content content = new Content("text/html",
"<h1>Привет, " + userName + "!</h1>");
Mail mail = new Mail(from, subject, to, content);
SendGrid sendGrid = new SendGrid(sendGridApiKey);
Request request = new Request();
try {
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());
Response response = sendGrid.api(request);
if (response.getStatusCode() != 202) {
log.error("SendGrid ошибка: " + response.getBody());
}
} catch (IOException e) {
log.error("SendGrid исключение", e);
}
}
}
Когда использовать:
- SendGrid/Mailgun — массовые письма, легко масштабируется
- AWS SES — интеграция с AWS экосистемой
- Встроенный SMTP — только для тестирования
2. SMS и Push уведомления
Twilio для SMS
@Service
public class SmsNotificationService {
private final Twilio twilio;
@Value("${twilio.phone-number}")
private String fromPhoneNumber;
public void sendSms(String toPhoneNumber, String message) {
Message response = Message.creator(
new PhoneNumber(toPhoneNumber), // To number
new PhoneNumber(fromPhoneNumber), // From number
message) // SMS body
.create();
log.info("SMS отправлено, SID: " + response.getSid());
}
}
Firebase Cloud Messaging (FCM) для Push
@Service
public class PushNotificationService {
@Autowired
private FirebaseMessaging firebaseMessaging;
public void sendPushNotification(
String deviceToken,
String title,
String body) throws FirebaseException {
Notification notification = Notification.builder()
.setTitle(title)
.setBody(body)
.setImage("https://example.com/image.png")
.build();
Message message = Message.builder()
.setToken(deviceToken)
.setNotification(notification)
.putData("click_action", "FLUTTER_NOTIFICATION_CLICK")
.build();
String response = firebaseMessaging.send(message);
log.info("Push отправлен, message ID: " + response);
}
}
3. Message Queue для надежной доставки
Kafka для асинхронной доставки уведомлений
// Отправить событие в Kafka
@Service
public class NotificationEventPublisher {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public void publishNotificationEvent(String userId, String message) {
String event = String.format(
"{\"userId\": \"%s\", \"message\": \"%s\"}",
userId, message);
kafkaTemplate.send("notification-events", userId, event);
log.info("Событие отправлено в Kafka");
}
}
// Слушатель обрабатывает события
@Service
public class NotificationEventListener {
@Autowired
private EmailNotificationService emailService;
@KafkaListener(topics = "notification-events")
public void listen(String message) {
try {
// Парсим JSON
JSONObject json = new JSONObject(message);
String userId = json.getString("userId");
String notificationMessage = json.getString("message");
// Отправляем
emailService.sendEmail(userId, notificationMessage);
} catch (Exception e) {
log.error("Ошибка обработки события", e);
// Kafka автоматически повторит
}
}
}
RabbitMQ с Dead Letter Queue для retry
@Configuration
public class RabbitMqConfig {
public static final String NOTIFICATION_QUEUE = "notification-queue";
public static final String DLQ_QUEUE = "notification-dlq";
@Bean
public Queue notificationQueue() {
return QueueBuilder.durable(NOTIFICATION_QUEUE)
.deadLetterExchange("dlx")
.deadLetterRoutingKey("dlq")
.build();
}
@Bean
public Queue dlqQueue() {
return QueueBuilder.durable(DLQ_QUEUE).build();
}
}
// Публикатор
@Service
public class NotificationPublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendNotification(String message) {
rabbitTemplate.convertAndSend(NOTIFICATION_QUEUE, message);
}
}
// Слушатель с автоматическим retry
@Service
public class NotificationListener {
@RabbitListener(queues = NOTIFICATION_QUEUE)
public void listen(String message) {
// Если упадет -> RabbitMQ повторит
processNotification(message);
}
}
4. Базовый подход в приложении
Паттерн: Notification Service + Background Queue
// Доменная модель
@Entity
@Table(name = "notifications")
public class Notification {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
@Column(nullable = false)
private Long userId;
@Column(nullable = false)
private String type; // EMAIL, SMS, PUSH
@Column(nullable = false)
private String content;
@Enumerated(EnumType.STRING)
private NotificationStatus status; // PENDING, SENT, FAILED
@Column(nullable = false)
private LocalDateTime createdAt;
private LocalDateTime sentAt;
private String errorMessage;
}
// Сервис
@Service
public class NotificationService {
@Autowired
private NotificationRepository notificationRepository;
@Autowired
private EmailNotificationService emailService;
@Autowired
private SmsNotificationService smsService;
// Создать уведомление (асинхронно обработается)
@Transactional
public void notifyUser(Long userId, String type, String content) {
Notification notification = new Notification();
notification.setUserId(userId);
notification.setType(type);
notification.setContent(content);
notification.setStatus(NotificationStatus.PENDING);
notification.setCreatedAt(LocalDateTime.now());
notificationRepository.save(notification);
// Отправить в очередь для обработки
processNotification(notification);
}
@Async
private void processNotification(Notification notification) {
try {
User user = findUser(notification.getUserId());
switch (notification.getType()) {
case "EMAIL":
emailService.send(user.getEmail(), notification.getContent());
break;
case "SMS":
smsService.send(user.getPhoneNumber(), notification.getContent());
break;
case "PUSH":
// send push
break;
}
notification.setStatus(NotificationStatus.SENT);
notification.setSentAt(LocalDateTime.now());
} catch (Exception e) {
notification.setStatus(NotificationStatus.FAILED);
notification.setErrorMessage(e.getMessage());
log.error("Ошибка при отправке уведомления", e);
} finally {
notificationRepository.save(notification);
}
}
}
5. Сравнение подходов
| Способ | Надежность | Масштабируемость | Когда использовать |
|---|---|---|---|
| SMTP напрямую | Низкая | Низкая | Тесты |
| SendGrid/Mailgun | Высокая | Высокая | |
| Twilio | Высокая | Высокая | SMS |
| Firebase | Высокая | Высокая | Push notifications |
| Kafka | Высокая | Очень высокая | Масса event-driven уведомлений |
| RabbitMQ | Высокая | Высокая | Reliable delivery с retry |
6. Best Practices
1. Никогда не отправляй синхронно из HTTP обработчика
// ПЛОХО: синхронно из handler
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody UserRequest req) {
User user = userService.create(req);
emailService.send(user.getEmail(), "Welcome!"); // блокирует ответ!
return ResponseEntity.ok(user);
}
// ХОРОШО: отправить в очередь
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody UserRequest req) {
User user = userService.create(req);
notificationService.notifyAsync(user.getId(), "welcome"); // async
return ResponseEntity.ok(user);
}
2. Делай retry и Dead Letter Queue
// Kafka не обрабатывает успешно?
// -> отправляется в DLQ
// -> позже manual review
3. Логируй все уведомления
// Это спасает когда юзер говорит "я не получал письмо"
notification.setStatus(SENT);
notification.setSentAt(now);
notification.setProvider("SendGrid");
notification.setProviderId(response.getMessageId());
notificationRepository.save(notification);
4. Ограничивай rate
// Не отправляй 100 писем одному юзеру в минуту
@RateLimiter(name = "notification-limiter")
public void sendEmail(String email, String message) {
// ...
}
Мой стандартный стек
- Email → SendGrid (надежно, легко)
- SMS → Twilio (стандарт индустрии)
- Push → Firebase Cloud Messaging
- Очередь → Kafka или RabbitMQ (зависит от требований)
- Хранение → БД таблица notifications для истории
Вывод
Отправка уведомлений — это не просто вызов SMTP. Это целая система:
- Асинхронность (не блокируй основной поток)
- Надежность (retry, DLQ, логирование)
- Масштабируемость (используй очереди)
- Мониторинг (знай, что произошло с каждым уведомлением)
В боевых проектах это один из самых критичных компонентов, потому что потеря уведомления = потеря пользователя.