Что произойдёт при вызове @Transactional метода из другого метода того же класса?
Если вы вызываете @Transactional метод из другого метода того же класса — аннотация будет проигнорирована, и транзакция не начнётся.
🟢 Junior Level
Если вы вызываете @Transactional метод из другого метода того же класса — аннотация будет проигнорирована, и транзакция не начнётся.
Это называется Self-invocation problem (проблема самовызова).
Простой пример
@Service
public class OrderService {
public void createOrderExternal() {
saveOrder(); // Вызов внутри класса — @Transactional НЕ работает!
}
@Transactional
public void saveOrder() {
// Эта логика выполнится БЕЗ транзакции
orderRepository.save(order);
}
}
Аналогия
Представьте что вы звоните другу через секретаря (Proxy). Секретарь записывает разговор (управляет транзакцией). Но если вы крикните другу через стену (this) — секретарь ничего не знает о разговоре.
Почему это происходит
Spring управляет транзакциями через Proxy-объект (обёртку). Когда внешний код вызывает метод — он идёт через прокси. Но когда метод вызывает другой метод того же класса — вызов идёт напрямую через this, минуя прокси.
Механизм: Spring создаёт прокси-объект, который перехватывает вызовы извне. Но когда метод A вызывает метод B того же объекта, this.methodB() идёт напрямую — прокси не участвует, TransactionInterceptor не срабатывает.
Важные правила
@Transactionalработает только при вызове извне классаthis.method()— всегда bypass proxy- Private/protected методы никогда не перехватываются
Как решить
- Вынести метод в другой бин (лучший вариант)
- Self-injection: внедрить сервис сам в себя с
@Lazy - TransactionTemplate: программное управление транзакциями
Когда self-invocation НЕ проблема
- Метод не требует транзакции — если @Transactional стоит для консистентности, но метод read-only
- Внешний вызов уже в транзакции — если outerMethod уже @Transactional, innerMethod работает в той же транзакции (propagation = REQUIRED по умолчанию)
- Используете AspectJ mode — compile-time/load-time weaving перехватывает
thisвызовы
🟡 Middle Level
Механизм Proxy
Внешний вызов:
Client → Proxy → TransactionInterceptor → TransactionManager → Реальный метод
↓
Открыть транзакцию → Выполнить → COMMIT/ROLLBACK
Внутренний вызов (this):
Метод A → this.метод B() → Реальный метод (минуя Proxy!)
↓
Нет транзакции!
Spring создаёт бин-обёртку (Proxy) вокруг вашего объекта. Когда внешний клиент вызывает метод, он вызывает его у Proxy. Proxy перехватывает вызов, открывает транзакцию и передаёт управление.
Вызов через this идёт напрямую к реальному объекту — proxy полностью bypassed.
Способы решения
А. Вынос метода в другой бин (Рекомендуемый)
@Service
public class OrderService {
@Autowired
private OrderRepositoryService orderRepositoryService;
public void createOrderExternal() {
orderRepositoryService.saveOrder(); // Через другой бин — работает!
}
}
@Service
public class OrderRepositoryService {
@Transactional
public void saveOrder() {
// Теперь транзакция работает
}
}
Б. Self-injection
@Service
public class OrderService {
@Autowired
@Lazy // Чтобы избежать циклической зависимости
private OrderService self;
public void createOrderExternal() {
self.saveOrder(); // Через proxy — транзакция работает!
}
@Transactional
public void saveOrder() {
// Теперь транзакция работает
}
}
В. TransactionTemplate
@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
public void createOrderExternal() {
transactionTemplate.execute(status -> {
saveOrder(); // Программная транзакция
return null;
});
}
public void saveOrder() {
// Выполняется в транзакции через TransactionTemplate
}
}
Г. AspectJ
Переход на AspectJ weaving модифицирует байт-код при компиляции, и вызовы внутри класса перехватываются корректно. Требует сложной настройки.
Распространённые ошибки
| Ошибка | Что происходит | Как исправить |
|---|---|---|
| Не знать о self-invocation | Код работает в тестах, падает в production | Вынести в отдельный бин |
Self-injection без @Lazy |
BeanCurrentlyInCreationException |
Добавить @Lazy |
AopContext.currentProxy() без exposeProxy |
IllegalStateException: Cannot find current proxy |
@EnableAspectJAutoProxy(exposeProxy = true) |
| Смешивание подходов | Непредсказуемое поведение | Выбрать один подход |
Сравнение решений
| Решение | Сложность | Self-invocation | Private методы | Рекомендуется |
|---|---|---|---|---|
| Вынос в отдель бин | Низкая | ✅ | ❌ | ✅ Лучший |
| Self-injection (@Lazy) | Низкая | ✅ | ❌ | Хороший |
| AopContext.currentProxy() | Средняя | ✅ | ❌ | OK |
| TransactionTemplate | Средняя | ✅ | ✅ | Хороший для сложной логики |
| AspectJ weaving | Высокая | ✅ | ✅ | Только если нужно |
Когда НЕ использовать решения
| Ситуация | Почему | Альтернатива |
|---|---|---|
| Метод не требует отдельной транзакции | Self-invocation не проблема если caller уже в tx | Оставить как есть |
| Simple read-only метод | ReadOnly не требует сложной транзакции | readOnly = true на caller |
| Метод — утилитарный, не бизнес-логика | Не belongs в сервисном слое | Вынести в util class |
🔴 Senior Level
Proxy-Based AOP — Root Cause Analysis
How Spring Creates Proxies
JDK Dynamic Proxy:
UserService proxy = (UserService) Proxy.newProxyInstance(
classLoader,
new Class[]{UserService.class},
new TransactionalInvocationHandler(target, interceptor)
);
CGLIB:
UserService proxy = (UserService) Enhancer.create(
UserService.class,
new TransactionalMethodInterceptor(target, interceptor)
);
В обоих случаях proxy оборачивает target object. Внешние вызовы идут через proxy → interceptor → target. Внутренние вызовы (this.method()) идут напрямую: target → target. Это работает и для JDK Dynamic Proxy, и для CGLIB — в обоих случаях this ссылается на целевой объект, не на прокси. CGLIB создаёт подкласс, но this внутри методов подкласса всё равно указывает на целевой объект.
The this Reference Problem
public class OrderService {
// this = OrderService@123 (the TARGET object, not the proxy)
public void createOrderExternal() {
// this.saveOrder() вызывает target напрямую
// Bypasses: OrderServiceProxy@456.saveOrder()
saveOrder();
}
}
this всегда указывает на actual object, never на proxy. Нет способа для Spring перехватить this.*() вызовы со стандартным AOP.
Solution Deep Dive
Solution 1: Self-Injection with @Lazy
@Service
public class OrderService {
@Autowired
@Lazy
private OrderService self;
public void createOrderExternal() {
// self = proxy (OrderService$$EnhancerBySpringCGLIB$$...)
self.saveOrder(); // Goes through proxy → interceptor
}
}
Why @Lazy: Без @Lazy, Spring пытается инжектировать OrderService во время его создания → circular dependency error. @Lazy создаёт lazy proxy, который резолвит реальный бин при первом использовании.
Solution 2: AopContext.currentProxy()
@EnableAspectJAutoProxy(exposeProxy = true) // Required!
@Service
public class OrderService {
public void createOrderExternal() {
((OrderService) AopContext.currentProxy()).saveOrder();
}
}
How it works: exposeProxy = true сохраняет proxy в ThreadLocal. AopContext.currentProxy() извлекает его.
Drawbacks: Tight coupling к Spring API, ThreadLocal — не работает across threads, exposeProxy имеет performance overhead.
// exposeProxy = true добавляет overhead на каждый вызов (ThreadLocal). // Не используйте в highload. Альтернатива: вынесите метод в отдельный бин.
Solution 3: Programmatic Transactions
@Service
public class OrderService {
@Autowired
private PlatformTransactionManager transactionManager;
public void createOrderExternal() {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.execute(status -> {
saveOrder(); // In transaction
return null;
});
}
}
Advantages: No proxy needed, explicit transaction boundaries, работает в любом контексте. Disadvantages: More verbose, mixing transaction management с business logic.
AspectJ Weaving — The Nuclear Option
Compile-Time Weaving
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<configuration>
<aspectLibraries>
<aspectLibrary>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</aspectLibrary>
</aspectLibraries>
</configuration>
</plugin>
Модифицирует bytecode at compile time:
// BEFORE
public void createOrderExternal() { saveOrder(); }
// AFTER (AspectJ-woven)
public void createOrderExternal() {
TransactionInterceptor.invoke(this, "saveOrder", ...);
saveOrder();
}
Benefits: Self-invocation работает, private/protected методы могут быть @Transactional, нет proxy limitations.
Drawbacks: Complex build config, bytecode modification (debugging harder), limited IDE support, не совместим со всеми Spring Boot features.
Edge Cases (минимум 3)
-
@Transactionalна интерфейсе + CGLIB: Если proxy — CGLIB (class-based), аннотация на интерфейсе не видна. Self-invocation уже не работает, плюс аннотация не применяется к proxy. Двойная проблема. -
Nested self-invocation:
methodA()→this.methodB()(no tx) →this.methodC()(has@Transactional). Ни B, ни C не будут транзакционными. Вся цепочка bypassed. -
@Transactional(propagation = REQUIRES_NEW)при self-invocation: ДажеREQUIRES_NEWне сработает приthis.method()вызове. Новая транзакция не откроется — метод выполнится в контексте caller (без транзакции если caller тоже не в tx). -
Self-invocation и
@Async:@Asyncметоды тоже идут через proxy.this.asyncMethod()— метод выполнится синхронно, не асинхронно. Двойной proxy bypass: и транзакция, и async. -
Constructor вызовы: Если конструктор вызывает
@Transactionalметод — proxy ещё не создан (bean initialization не завершён). Вызов всегда идёт напрямую.
JDK Proxy vs CGLIB для Self-Invocation
| Характеристика | JDK Dynamic Proxy | CGLIB |
|---|---|---|
| Self-invocation | ❌ Не работает | ❌ Не работает |
| Final методы | N/A (interface) | ❌ Не перехватываются |
| Private методы | ❌ Не перехватываются | ❌ Не перехватываются |
| Performance overhead | Низкий | Средний |
| Bytecode generation | Нет | Да (генерирует subclass) |
Оба типа proxy не решают self-invocation проблему. Только AspectJ weaving решает.
Performance Numbers
| Операция | Время |
|---|---|
| Proxy invocation overhead | ~1-2 μs |
| AopContext.currentProxy() lookup | ~0.5 μs (ThreadLocal get) |
| exposeProxy overhead | ~0.1 μs per call |
| Self-injection (@Lazy) resolution | ~1 μs (first call), 0 (cached) |
Memory Implications
exposeProxy = true: ThreadLocal для каждого proxy — ~1 KB per active thread- Self-injection:额外 proxy reference в бине — negligible (~8 bytes)
- AspectJ weaving: Bytecode modification — larger class files, no runtime memory impact
Thread Safety
AopContext.currentProxy() использует ThreadLocal — не работает across threads. @Async методы в другом потоке не получат proxy из ThreadLocal caller. Self-injection proxy — thread-safe (singleton proxy reference).
Production War Story
Сервис обработки заказов: createOrder() вызывал this.validateOrder() (с @Transactional). Валидация писала audit log в БД. В тестах (один поток, без конкуренции) — работало. В production — audit log не писался, т.к. транзакция не открывалась. Данные терялись. Fix: validateOrder() вынесен в отдельный OrderValidationService.
Monitoring
Detect self-invocation at runtime:
@Bean
public BeanPostProcessor selfInvocationDetector() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (AopUtils.isAopProxy(bean)) {
// Log: this bean has a proxy — self-invocation will bypass it
}
return bean;
}
};
}
Actuator:
GET /actuator/beans # Показывает какие бины обернуты в proxy
Highload Best Practices
- Extract to separate bean — always: Это не workaround, это proper separation of concerns. Если методу нужна своя транзакция — у него должна быть своя responsibility.
- Avoid
AopContext.currentProxy(): Tight coupling, ThreadLocal issues, performance overhead. Не для production. - Self-invocation = code smell: Если боретесь с self-invocation — сервис, вероятно, делает слишком много. Split по SRP.
- Test self-invocation behavior: Unit test с Mockito не ловит эту проблему (Mockito mocks идут через proxy). Нужен Spring integration test.
- Consider programmatic transactions: Для сложной условной логики
TransactionTemplateчище чем fighting с proxy. - Architecture rule: Ввести ArchUnit test:
noClass().should().callMethodWhere(target declaresAnnotation(Transactional.class)).fromSameClass().
🎯 Шпаргалка для интервью
Обязательно знать:
- Self-invocation problem: вызов @Transactional метода через
thisbypass-ит Spring Proxy - Spring управляет транзакциями через AOP Proxy (JDK Dynamic Proxy или CGLIB)
this.method()идёт напрямую к target object — TransactionInterceptor не срабатывает- 4 решения: вынести в другой бин (best), self-injection @Lazy, AopContext.currentProxy(), TransactionTemplate
- REQUIRES_NEW тоже НЕ работает при self-invocation — новая транзакция не откроется
- @Async при self-invocation выполняется синхронно — двойной proxy bypass
Частые уточняющие вопросы:
- Почему proxy не перехватывает this вызовы? —
thisвсегда ссылается на target object, не на proxy - Чем JDK Proxy отличается от CGLIB в контексте self-invocation? — Оба НЕ решают проблему self-invocation
- Когда self-invocation НЕ проблема? — Если caller уже в @Transactional (propagation REQUIRED join-ит)
- Почему Mockito не ловит эту проблему? — Mockito mocks = proxy, self-invocation работает на mock-е
Красные флаги (НЕ говорить):
- “Self-invocation работает в production” — proxy bypassed, транзакция не открывается
- “REQUIRES_NEW решает self-invocation” — REQUIRES_NEW тоже требует proxy
- “AopContext.currentProxy() = best practice” — tight coupling к Spring API, ThreadLocal overhead
Связанные темы:
- [[16. Что такое аннотация @Transactional]]
- [[13. Что такое Propagation в Spring]]
- [[17. На каком уровне можно использовать @Transactional]]
- [[15. В чём разница между REQUIRED и REQUIRES_NEW]]