Proxies: JDK, CGLIB, and what silently is not advised

Spring Boot · lesson 30 of 39 · 6 min read

Why a final method loses its transaction and nothing warns you about it.

Open this lesson in the learning hub

Key points

  • Spring AOP is proxy-based. Your bean is wrapped, and the wrapper is what gets injected everywhere else - the interceptor lives in the wrapper, not in your class.
  • If the class implements an interface, Spring can build a JDK dynamic proxy, which implements that interface and delegates. Only interface methods are advised.
  • With no interface, Spring falls back to CGLIB, which generates a subclass and overrides each method. This is why final classes cannot be proxied and final or private methods are never advised.
  • That failure is silent. A final @Transactional method compiles, runs, and simply has no transaction. Nothing logs a warning.
  • A CGLIB proxy has its own fields, all null - it delegates rather than copying state. Reading a field directly off an injected bean therefore sees null, while calling a getter works.
  • @Configuration classes are CGLIB-proxied too, which is what makes one @Bean method calling another return the same singleton. Setting proxyBeanMethods = false removes that proxy and each call constructs a new object.

Example

// 1. No interface -> CGLIB subclass. This method is NOT advised, and nothing says so.
@Service
public class ReportService {

    @Transactional
    public final void generate() {      // final: CGLIB cannot override it
        // ... runs with no transaction at all
    }
}

// 2. Self-invocation bypasses the proxy for the same reason.
@Service
public class OrderService {

    public void placeAll(List<Order> orders) {
        orders.forEach(this::placeOne);   // "this" is the raw object, not the proxy
    }

    @Transactional
    public void placeOne(Order order) {  // no transaction when called as above
        // ...
    }
}

// 3. Proving what you actually hold.
@Component
class ProxyReporter implements CommandLineRunner {

    private final OrderService orders;
    ProxyReporter(OrderService orders) { this.orders = orders; }

    @Override
    public void run(String... args) {
        System.out.println(orders.getClass().getName());
        // com.example.OrderService$$SpringCGLIB$$0   <- proxied
        // com.example.OrderService                   <- NOT proxied, advice will not run
        System.out.println(AopUtils.isCglibProxy(orders));
    }
}

// 4. Configuration lite mode: each call builds a NEW object.
@Configuration(proxyBeanMethods = false)
class CacheConfig {

    @Bean Clock clock() { return Clock.systemUTC(); }

    @Bean
    Sweeper sweeper() {
        return new Sweeper(clock());     // a second Clock, not the bean above
    }
}

Print getClass().getName() on a bean whose advice is not firing - the absence of $$SpringCGLIB$$ is usually the whole answer.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Boot course, and every lesson in it is listed on the Spring Boot contents page.