Proxies: JDK, CGLIB, and what silently is not advised
Why a final method loses its transaction and nothing warns you about it.
Open this lesson in the learning hubKey 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
finalclasses cannot be proxied andfinalorprivatemethods are never advised. - That failure is silent. A
final@Transactionalmethod 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.
@Configurationclasses are CGLIB-proxied too, which is what makes one@Beanmethod calling another return the same singleton. SettingproxyBeanMethods = falseremoves 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.