Proxies and Self-Invocation

JDK Proxy vs CGLIB

JDK dynamic proxyCGLIB proxy
Works oninterfaces onlyclasses (creates a subclass at runtime)
Spring Boot defaultnoyes (spring.aop.proxy-target-class=true)
Cannot advisemethods not in the interfacefinal 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 call self.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.