WHY does Spring “skip” the annotation? What is actually happening in memory?
🧠 Step 1 — What Spring really creates
You write:
@Service
public class OrderService {
@Transactional
public void placeOrder() { }
@CacheEvict(value = "productCache", key = "#productId")
public void updateProduct(int productId) { }
}
❗ What actually exists at runtime
Spring creates two objects:
1. Real Object:
OrderService
2. Proxy Object:
OrderService$$SpringProxy
👉 Important
When you do:
@Autowired
OrderService orderService;
👉 You are getting:
OrderService$$SpringProxy
NOT your real class.
🧩 Step 2 — What’s inside the proxy?
The proxy wraps your method calls like this:
public class OrderServiceProxy {
private OrderService target;
public void updateProduct(int productId) {
// 🔥 Cache logic injected here
evictCache("productCache", productId);
target.updateProduct(productId);
}
public void placeOrder() {
startTransaction();
try {
target.placeOrder();
commit();
} catch (Exception e) {
rollback();
}
}
}
👉 Your annotations are implemented HERE, not in your class.
🎯 Step 3 — Real Scenario (actual flow)
Scenario: Updating product + clearing cache
✅ Case 1 — External call (works)
@RestController
class ProductController {
@Autowired
OrderService orderService;
public void update() {
orderService.updateProduct(10);
}
}
🔁 Execution Flow
Controller
↓
OrderServiceProxy.updateProduct(10)
↓
🔥 Proxy sees @CacheEvict
↓
CacheManager.evict("productCache", 10)
↓
Calls real method:
OrderService.updateProduct(10)
✔ Cache cleared
✔ Everything works
❌ Case 2 — Internal call (your issue)
@Service
public class OrderService {
public void placeOrder() {
updateProduct(10); // ❌ internal call
}
@CacheEvict(value = "productCache", key = "#productId")
public void updateProduct(int productId) { }
}
🔁 Execution Flow
Controller
↓
OrderServiceProxy.placeOrder()
↓
calls real method:
OrderService.placeOrder()
↓
inside method:
this.updateProduct(10)
↓
DIRECT call to method
🚨 CRITICAL POINT
At this moment:
this = REAL OBJECT (OrderService)
NOT proxy
So call becomes:
OrderService.updateProduct(10)
NOT:
OrderServiceProxy.updateProduct(10)
❗ Result
- Proxy is completely bypassed
-
@CacheEvictis never seen - Cache is NOT cleared
🧠 WHY does this happen?
Because of how Java works, not just Spring.
🔬 Inside the JVM
When you write:
updateProduct(10);
Compiler converts it roughly to:
this.updateProduct(10);
👉 That means:
“Call method on the current object in memory”
And what is this?
Inside your class:
this = OrderService (real object)
NOT:
OrderServiceProxy
🧠 Key Insight
The proxy only exists outside your object
Once execution enters your class:
You are inside the real object
Proxy is gone from the picture
🔁 Visual Timeline
External call
[Controller]
↓
[Proxy] ← interception happens here
↓
[Real Object]
Internal call
[Real Object]
↓
[Real Object method]
🚫 Proxy never involved
🎯 Same thing with @Transactional
Example
@Service
public class PaymentService {
public void checkout() {
chargeCard(); // ❌
}
@Transactional
public void chargeCard() { }
}
Expected
checkout → chargeCard → transaction starts
Actual
checkout (proxy)
↓
real checkout()
↓
this.chargeCard()
↓
real chargeCard()
🚫 No transaction
🚫 No rollback
🚫 No protection
🧠 Why Spring doesn’t “fix” this?
Because:
- Spring uses proxies, not bytecode rewriting
- It doesn’t modify your class
- It wraps it externally
👉 So it cannot intercept internal calls
🔥 The Real Rule (technical wording)
Spring AOP is proxy-based, not self-invocation aware
🧪 If you want proof (try this)
Add this:
System.out.println(this.getClass());
Inside your method.
You’ll see:
class com.yourapp.OrderService
NOT:
OrderService$$SpringProxy
🧠 Final Mental Model
- Proxy = outer wrapper
- Your class = inner core
Rule:
👉 Only calls entering from outside hit the proxy
👉 Calls inside the class stay inside the core
🔥 One-line explanation
It “skips” because internal calls use
this, andthisrefers to the real object — not the proxy where Spring put the annotation logic.
Comments
Post a Comment