Що станеться при виклику @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]]