Эта служба Spring, EmailsOutgoingService, отвечает за обработку исходящих писем и запланирована на запуск каждые 60 секунд с использованием аннотации @Scheduled(fixedRate = 60000). По сути, он проверяет статус электронного письма в очереди и выполняет операции с существующей записью или создает другую. Проблема в том, что транзакция «repository.save» не фиксируется в базе данных.
@Service
public class EmailsOutgoingService {
@Autowired
private JavaMailSender emailSender;
@Autowired
private EmailsOutgoingRepository repository;
@Scheduled(fixedRate = 60000)
@Transactional
public void processEmails() {
System.out.println("Processing emails...");
List<EmailsOutgoing> pendingEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
List<EmailsOutgoing> failedEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_FAILED);
for (EmailsOutgoing pendingEmail : pendingEmails) {
System.out.println("Processing pending email with ID: " + pendingEmail.getId());
sendEmail(pendingEmail);
}
for (EmailsOutgoing failedEmail : failedEmails) {
System.out.println("Processing failed email with ID: " + failedEmail.getId());
retryFailedEmail(failedEmail);
}
}
public void sendEmail(EmailsOutgoing email) {
try {
System.out.println("Sending email...");
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email.getEmailReceivers().split(","));
message.setSubject(email.getEmailSubject());
message.setText(email.getEmailBody());
emailSender.send(message);
email.setDeliveryStatus(EmailsOutgoing.STATUS_DELIVERED);
repository.save(email);
System.out.println("Email sent successfully, status updated to DELIVERED with ID: " + email.getId());
} catch (Exception e) {
System.out.println("Failed to send email: " + e.getMessage());
email.setDeliveryStatus(EmailsOutgoing.STATUS_FAILED);
email.setEmailMessage(e.getMessage());
repository.save(email);
System.out.println("Email failed, status updated to FAILED with ID: " + email.getId());
EmailsOutgoing newEmail = new EmailsOutgoing();
newEmail.setEmailSubject(email.getEmailSubject());
newEmail.setEmailBody(email.getEmailBody());
newEmail.setDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
newEmail.setEmailEntity(email.getEmailEntity());
newEmail.setClientCode(email.getClientCode());
newEmail.setEmailReceivers(email.getEmailReceivers());
repository.save(newEmail);
System.out.println("Created new pending email with ID: " + newEmail.getId());
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void retryFailedEmail(EmailsOutgoing failedEmail) {
System.out.println("Retrying failed email...");
EmailsOutgoing newEmail = new EmailsOutgoing();
newEmail.setEmailSubject(failedEmail.getEmailSubject());
newEmail.setEmailBody(failedEmail.getEmailBody());
newEmail.setDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
newEmail.setEmailEntity(failedEmail.getEmailEntity());
newEmail.setClientCode(failedEmail.getClientCode());
newEmail.setEmailReceivers(failedEmail.getEmailReceivers());
repository.save(newEmail);
System.out.println("Created new pending email with ID: " + newEmail.getId());
sendEmail(newEmail);
}
}
Если я попытаюсь вызвать метод почтальона, все будет работать нормально. Я пришел к выводу, что @Scheduled не совершает транзакцию. Как можно решить проблему? Я также попытался создать отдельный класс с помощью @Scheduled, который вызывает класс обслуживания, выполняющий операции сохранения, но он не работает:
Вызов метода с помощью @Scheduler из другого класса:
Планировщик исходящих писем:
@Component
public class EmailsOutgoingScheduler {
@Autowired
private EmailsOutgoingService emailsOutgoingService;
@Scheduled(fixedRate = 60000)
public void checkForNewNotifications() {
System.out.println("Checking for new notifications...");
emailsOutgoingService.processEmails();
}
}
Служба исходящих сообщений электронной почты:
@Service
public class EmailsOutgoingService {
@Autowired
private JavaMailSender emailSender;
@Autowired
private EmailsOutgoingRepository repository;
public void processEmails() {
System.out.println("Processing emails...");
List<EmailsOutgoing> pendingEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
List<EmailsOutgoing> failedEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_FAILED);
for (EmailsOutgoing pendingEmail : pendingEmails) {
System.out.println("Processing pending email with ID: " + pendingEmail.getId());
sendEmail(pendingEmail);
}
for (EmailsOutgoing failedEmail : failedEmails) {
System.out.println("Processing failed email with ID: " + failedEmail.getId());
retryFailedEmail(failedEmail);
}
}
@Transactional
public void sendEmail(EmailsOutgoing email) {
try {
System.out.println("Sending email...");
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(email.getEmailReceivers().split(","));
message.setSubject(email.getEmailSubject());
message.setText(email.getEmailBody());
emailSender.send(message);
email.setDeliveryStatus(EmailsOutgoing.STATUS_DELIVERED);
repository.save(email); // Update status to delivered
System.out.println("Email sent successfully, status updated to DELIVERED with ID: " + email.getId());
} catch (Exception e) {
System.out.println("Failed to send email: " + e.getMessage());
email.setDeliveryStatus(EmailsOutgoing.STATUS_FAILED);
email.setEmailMessage(e.getMessage());
repository.save(email); // Update status to failed
System.out.println("Email failed, status updated to FAILED with ID: " + email.getId());
// Create a new pending email record to retry
EmailsOutgoing newEmail = new EmailsOutgoing();
newEmail.setEmailSubject(email.getEmailSubject());
newEmail.setEmailBody(email.getEmailBody());
newEmail.setDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
newEmail.setEmailEntity(email.getEmailEntity());
newEmail.setClientCode(email.getClientCode());
newEmail.setEmailReceivers(email.getEmailReceivers());
repository.save(newEmail);
System.out.println("Created new pending email with ID: " + newEmail.getId());
}
}
@Transactional
public void retryFailedEmail(EmailsOutgoing failedEmail) {
System.out.println("Retrying failed email...");
// The retry logic here is to create a new pending email record and then try sending it
EmailsOutgoing newEmail = new EmailsOutgoing();
newEmail.setEmailSubject(failedEmail.getEmailSubject());
newEmail.setEmailBody(failedEmail.getEmailBody());
newEmail.setDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
newEmail.setEmailEntity(failedEmail.getEmailEntity());
newEmail.setClientCode(failedEmail.getClientCode());
newEmail.setEmailReceivers(failedEmail.getEmailReceivers());
repository.save(newEmail);
System.out.println("Created new pending email with ID: " + newEmail.getId());
sendEmail(newEmail);
}
}
Журнал гибернации:
Checking for new notifications...
[restartedMain] INFO org.springframework.boot.devtools.autoconfigure.ConditionEvaluationDeltaLoggingListener - Condition evaluation unchanged
Processing emails...
[2024-06-19 17:13:32.011] - 23980 DEBUG [scheduling-1] --- org.hibernate.SQL: select emailsoutg0_.pk_email_outgoing as pk_email1_11_, emailsoutg0_.client_code as client_c2_11_, emailsoutg0_.email_outgoing_status as email_ou3_11_, emailsoutg0_.email_outgoing_body as email_ou4_11_, emailsoutg0_.email_outgoing_date as email_ou5_11_, emailsoutg0_.email_outgoing_entity as email_ou6_11_, emailsoutg0_.email_outgoing_message as email_ou7_11_, emailsoutg0_.email_outgoing_receivers as email_ou8_11_, emailsoutg0_.email_outgoing_subject as email_ou9_11_ from emails_outgoing emailsoutg0_ where emailsoutg0_.email_outgoing_status=?
[2024-06-19 17:13:32.034] - 23980 TRACE [scheduling-1] --- org.hibernate.type.descriptor.sql.BasicBinder: binding parameter [1] as [VARCHAR] - [Pending]
[2024-06-19 17:13:32.037] - 23980 DEBUG [scheduling-1] --- org.hibernate.SQL: select emailsoutg0_.pk_email_outgoing as pk_email1_11_, emailsoutg0_.client_code as client_c2_11_, emailsoutg0_.email_outgoing_status as email_ou3_11_, emailsoutg0_.email_outgoing_body as email_ou4_11_, emailsoutg0_.email_outgoing_date as email_ou5_11_, emailsoutg0_.email_outgoing_entity as email_ou6_11_, emailsoutg0_.email_outgoing_message as email_ou7_11_, emailsoutg0_.email_outgoing_receivers as email_ou8_11_, emailsoutg0_.email_outgoing_subject as email_ou9_11_ from emails_outgoing emailsoutg0_ where emailsoutg0_.email_outgoing_status=?
[2024-06-19 17:13:32.038] - 23980 TRACE [scheduling-1] --- org.hibernate.type.descriptor.sql.BasicBinder: binding parameter [1] as [VARCHAR] - [Failed]
Processing pending email with ID: 1
Sending email...
Email failed, status updated to FAILED with ID: 1
[2024-06-19 17:13:32.122] - 23980 DEBUG [scheduling-1] --- org.hibernate.SQL: select nextval ('emails_outgoing_sequence')
Created new pending email with ID: 866
Однажды я попал в такую же ловушку.
Транзакция не распространяется через прокси-сервер Spring, и управление транзакциями не происходит. Вам нужно вызвать общедоступный метод, помеченный @Transactional
, из другого класса.
@Autowired
private EmailProcessor email processor;
@Scheduled(fixedRate = 60000)
public void processEmails() {
System.out.println("Processing emails...");
emailProcessor.processEmails();
}
@Service
public class EmailProcessor {
// ...
@Transactional
public void processEmails() {
List<EmailsOutgoing> pendingEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_PENDING);
List<EmailsOutgoing> failedEmails = repository.findByDeliveryStatus(EmailsOutgoing.STATUS_FAILED);
for (EmailsOutgoing pendingEmail : pendingEmails) {
System.out.println("Processing pending email with ID: " + pendingEmail.getId());
sendEmail(pendingEmail);
}
for (EmailsOutgoing failedEmail : failedEmails) {
System.out.println("Processing failed email with ID: " + failedEmail.getId());
retryFailedEmail(failedEmail);
}
}
public void sendEmail(EmailsOutgoing email) {
// code
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void retryFailedEmail(EmailsOutgoing failedEmail) {
// code
}
}
Даже этот второй подход лишь увеличивает последовательность первичного ключа, но не фиксирует
Не могли бы вы предоставить некоторые журналы отладки из Hibernate?
Я снова обновил вопрос с журналом спящего режима
Весь processEmails
должен быть @Transactional
, потому что в противном случае нетранзакционный EmailsOutgoingService#processEmails
вызов @Transactional
методов в том же классе фактически обходит контроль управления транзакциями, поэтому аннотации sendEmail
и retryFailedEmail
игнорируются. Вы можете использовать TransactionTemplate
, чтобы обернуть тела методов sendEmail
и retryFailedEmail
для более точного управления транзакциями.
Даже попытка с TransactionTemplate не сработала. У меня тот же журнал Hibernate
Решение
Для тех, кто попал в ловушку использования @Scheduled для обновления данных в базе данных без решения:
Я решил обойти JPA, используя две функции PostgreSQL для создания и обновления записей. В сервисе ничего не изменилось, я просто вызываю функции из репозитория, используя аннотацию @Procedure.
@Transactional
@Procedure(procedureName = "updateOutgoingEmails")
int updateOutgoingEmails(@Param("p_id") Integer id,
@Param("p_deliveryStatus") String deliveryStatus,
@Param("p_emailMessage") String emailMessage);
@Transactional
@Procedure(procedureName = "createOutgoingEmails")
int createOutgoingEmails(@Param("p_emailSubject") String emailSubject,
@Param("p_emailBody") String emailBody,
@Param("p_emailEntity") String emailEntity,
@Param("p_clientCode") String clientCode,
@Param("p_emailReceivers") String emailReceivers,
@Param("p_deliveryStatus") String deliveryStatus,
@Param("p_emailMessage") String emailMessage);
Метод обновления должен быть недействительным, но при использовании аннотации @Modifying возникли ошибки, поэтому я решил сохранить возвращаемое значение, чтобы реализовать это раз и навсегда.
Привет, спасибо, что ответили. Это именно то, что я пытался сделать вторым подходом, но он не работает. Я обновил вопрос