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

Что произойдет при вызове публичного метода с @Transactional, который вызывает приватный метод с @Transactional REQUIRES_NEW?

2.0 Middle🔥 121 комментариев
#Spring Boot и Spring Data#Spring Framework

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

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

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

Spring @Transactional и приватные методы

Это коварный вопрос о том, как Spring AOP работает с приватными методами. Ответ вас удивит!

Краткий ответ

Аннотация @Transactional на приватном методе игнорируется, даже если она REQUIRES_NEW.

Почему? Spring использует динамические прокси, а приватные методы нельзя вызвать через прокси.

Как работают Spring прокси

Когда вы аннотируете класс с @Transactional, Spring создаёт прокси-обёртку:

@Service
public class UserService {
    @Transactional
    public void createUser(String name) {  // Публичный - работает!
        // Весь код внутри транзакции
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void sendNotification() {  // Приватный - игнорируется!
        // Это НЕ будет новой транзакцией
    }
}

// Spring создаёт прокси:
class UserServiceProxy extends UserService {
    @Override
    public void createUser(String name) {
        TransactionManager.begin();
        try {
            super.createUser(name);
            TransactionManager.commit();
        } catch (Exception e) {
            TransactionManager.rollback();
        }
    }
    
    // private методы вообще не оборачиваются!
}

Практический пример

@Service
public class UserService {
    
    @Transactional
    public void registerUser(String email, String password) {
        // Находимся в транзакции 1
        User user = new User(email, password);
        userRepository.save(user);  // Сохранено в БД
        
        // Вызов приватного метода - это обычный вызов Java!
        sendWelcomeEmail(user);  // НЕ создаёт новую транзакцию!
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void sendWelcomeEmail(User user) {
        // Это выполнится в ТОЙ ЖЕ транзакции, что и registerUser()
        // Аннотация @Transactional игнорируется!
        emailService.send(user.getEmail(), "Welcome!");
    }
}

// При вызове:
UserService service = applicationContext.getBean(UserService.class);
service.registerUser("user@example.com", "password");
// Оба метода выполняются в ОДНОЙ транзакции

Почему так происходит?

Spring прокси работает через интерфейсы (или CGLIB):

// Прокси могут перехватывать только публичные методы
UserServiceProxy implements UserService {
    @Override
    public void registerUser(String email, String password) {
        // Перехвачено - началась транзакция 1
        target.registerUser(email, password);
    }
}

// Внутри registerUser это уже обычный код Java
public void registerUser(String email, String password) {
    user = new User(email, password);
    userRepository.save(user);
    
    this.sendWelcomeEmail(user);  // this.sendWelcomeEmail = обычный вызов!
    // Даже если sendWelcomeEmail публичный!
}

Жизненный цикл:

  1. Вызывается proxy.registerUser() - перехвачено, начинается транзакция 1
  2. Внутри вызывается this.sendWelcomeEmail() - это call на реальный объект, не прокси!
  3. Прокси не знает об этом вызове
  4. @Transactional(REQUIRES_NEW) игнорируется

Решение 1: Сделать метод публичным

@Service
public class UserService {
    
    @Transactional
    public void registerUser(String email, String password) {
        User user = new User(email, password);
        userRepository.save(user);
        sendWelcomeEmail(user);  // Вызов публичного метода
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)  // ✅ Теперь работает!
    public void sendWelcomeEmail(User user) {
        emailService.send(user.getEmail(), "Welcome!");
    }
}

Решение 2: Самовызов через ApplicationContext

@Service
public class UserService {
    
    @Autowired
    private ApplicationContext context;
    
    @Transactional
    public void registerUser(String email, String password) {
        User user = new User(email, password);
        userRepository.save(user);
        
        // Вызываем через прокси!
        UserService proxy = context.getBean(UserService.class);
        proxy.sendWelcomeEmail(user);  // ✅ REQUIRES_NEW работает!
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendWelcomeEmail(User user) {
        emailService.send(user.getEmail(), "Welcome!");
    }
}

НО это плохая практика - усложняет тестирование!

Решение 3: Выделить в отдельный сервис

// Новый сервис для уведомлений
@Service
public class NotificationService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendWelcomeEmail(User user) {
        emailService.send(user.getEmail(), "Welcome!");
    }
}

// Основной сервис
@Service
public class UserService {
    
    @Autowired
    private NotificationService notificationService;
    
    @Transactional
    public void registerUser(String email, String password) {
        User user = new User(email, password);
        userRepository.save(user);
        
        // Вызов через впрыснутый сервис
        notificationService.sendWelcomeEmail(user);  // ✅ Новая транзакция!
    }
}

Это правильное решение - разделение ответственности!

Реальный пример: где это критично

@Service
public class OrderService {
    
    @Transactional
    public void processOrder(Order order) {
        // Транзакция 1
        order.setStatus(OrderStatus.PROCESSING);
        orderRepository.save(order);
        
        // Важно: если sendNotification() откатится,
        // заказ всё равно должен сохраниться!
        try {
            sendOrderConfirmation(order);  // Должна быть отдельной транзакцией
        } catch (Exception e) {
            // Логируем, но не откатываем заказ
            logger.error("Failed to send confirmation", e);
        }
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void sendOrderConfirmation(Order order) {
        // ❌ ЭТА АННОТАЦИЯ ИГНОРИРУЕТСЯ!
        emailService.send(order.getCustomer().getEmail(), "Order confirmed");
    }
}

// Результат: если emailService выбросит исключение,
// вся транзакция (включая сохранение заказа) откатится!
// Это ОШИБКА в дизайне!

Исправленная версия

@Service
public class NotificationService {
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void sendOrderConfirmation(Order order) {
        emailService.send(order.getCustomer().getEmail(), "Order confirmed");
    }
}

@Service
public class OrderService {
    
    @Autowired
    private NotificationService notificationService;
    
    @Transactional
    public void processOrder(Order order) {
        // Транзакция 1
        order.setStatus(OrderStatus.PROCESSING);
        orderRepository.save(order);  // Сохранится в любом случае
        
        // Отдельная транзакция
        try {
            notificationService.sendOrderConfirmation(order);
        } catch (Exception e) {
            logger.error("Failed to send confirmation", e);
            // Заказ уже сохранён, поэтому всё ОК
        }
    }
}

Проверка: всегда вводит в заблуждение

@Service
public class TestService {
    
    @Transactional
    public void publicMethod() {
        System.out.println("Transaction: " + TransactionSynchronizationManager.isActualTransactionActive());
        // true - мы в транзакции
        
        privateMethod();
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void privateMethod() {
        System.out.println("Transaction: " + TransactionSynchronizationManager.isActualTransactionActive());
        // true - но это ОДНА И ТА ЖЕ транзакция, не новая!
    }
}

Заключение

Ответ на вопрос:

  • Приватный метод с @Transactional(REQUIRES_NEW) игнорируется
  • Код выполняется в той же транзакции, что и вызывающий метод
  • Причина: Spring прокси не может перехватить приватные методы

Best Practice:

  1. Не используй @Transactional на приватных методах
  2. Если нужна отдельная транзакция - выдели в отдельный Service
  3. Впрысни этот сервис через @Autowired

Запомни: self-invocation (когда класс вызывает свой метод) всегда происходит вне прокси!