Careeroza
Proxies and Self-Invocation
JDK Proxy vs CGLIB
| JDK dynamic proxy | CGLIB proxy | |
|---|---|---|
| Works on | interfaces only | classes (creates a subclass at runtime) |
| Spring Boot default | no | yes (spring.aop.proxy-target-class=true) |
| Cannot advise | methods not in the interface | final classes/methods, private methods |
The Self-Invocation Trap
Advice runs only when the call goes through the proxy. A method calling another method of the same object uses this, which is the raw target, so the advice (and @Transactional, @Cacheable, @Async) is silently skipped.
Example
Proof in plain Java: the outer call is intercepted, the inner this. call is not:
import java.lang.reflect.*;
public class Main {
public static void main(String[] args) {
ProductService target = new ProductServiceImpl();
ProductService proxy = (ProductService) Proxy.newProxyInstance(
ProductService.class.getClassLoader(), new Class<?>[] { ProductService.class },
(p, m, a) -> {
System.out.println("[proxy] intercepted " + m.getName());
return m.invoke(target, a);
});
System.out.println("-- call refreshAll through the proxy --");
proxy.refreshAll();
System.out.println("-- call refreshOne through the proxy --");
proxy.refreshOne(7);
}
}
interface ProductService {
void refreshAll();
void refreshOne(long id);
}
class ProductServiceImpl implements ProductService {
public void refreshAll() {
System.out.println("refreshAll running");
this.refreshOne(1); // self-invocation: bypasses the proxy
}
public void refreshOne(long id) {
System.out.println("refreshOne(" + id + ") running");
}
}Output
-- call refreshAll through the proxy -- [proxy] intercepted refreshAll refreshAll running refreshOne(1) running -- call refreshOne through the proxy -- [proxy] intercepted refreshOne refreshOne(7) running
Fixes
- Best: move the inner method to another bean and inject it.
- Inject the bean into itself lazily:
@Lazy @Autowired ProductService self;then callself.refreshOne(id). - Use AspectJ compile-time or load-time weaving (rarely needed).
Note: This is the number one reason people say "my @Transactional is not working". Always check whether the call comes from inside the same class.