← Назад к вопросам

Через что отправляли уведомления

1.0 Junior🔥 101 комментариев
#REST API и микросервисы#Брокеры сообщений

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Системы отправки уведомлений в 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ВысокаяВысокаяEmail
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) {
    // ...
}

Мой стандартный стек

  1. Email → SendGrid (надежно, легко)
  2. SMS → Twilio (стандарт индустрии)
  3. Push → Firebase Cloud Messaging
  4. Очередь → Kafka или RabbitMQ (зависит от требований)
  5. Хранение → БД таблица notifications для истории

Вывод

Отправка уведомлений — это не просто вызов SMTP. Это целая система:

  • Асинхронность (не блокируй основной поток)
  • Надежность (retry, DLQ, логирование)
  • Масштабируемость (используй очереди)
  • Мониторинг (знай, что произошло с каждым уведомлением)

В боевых проектах это один из самых критичных компонентов, потому что потеря уведомления = потеря пользователя.