Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Learn › Spring › AOP and proxies

Proxies and self-invocation

Why does the annotation have no effect?

@Transactional and @Async do not change the method itself. Spring registers a proxy (a stand-in object) in place of the bean and adds the extra behavior only to calls that come in through the proxy. So depending on where and how the method is called, the same annotation is either applied or ignored.

Checked against real execution · All 10 combinations were run on Spring 7.0.9 and checked against the results

Concepts in this lab

Proxy
This is a stand-in object that receives calls in place of the real object. When it receives a call, it does some additional work and then passes the call on to the real object. Spring wraps a bean that needs a feature such as AOP or @Transactional in such an object and registers it in the container, so what other beans receive by injection is this stand-in, not the real object.
Self-invocation
This means that an object directly calls another of its own methods from inside the same object. Proxy-based features apply only to calls that come in from outside through the proxy, so settings such as @Transactional, @Async, and @Cacheable do not apply to a method that is called internally through this. The usual fix is to move that method to another bean.
Aspect-oriented programming (AOP)
This programming technique separates additional behavior that appears repeatedly in many places (crosscutting concerns), such as logging, transactions, and permission checks, from the core business code and gathers it in one place. You write the additional behavior only once and specify separately "where to apply it", so the core code stays clean and a change to that behavior is made in one place.
JDK dynamic proxy and CGLIB
These are the two ways Spring creates a proxy. A JDK dynamic proxy is a proxy that implements the interfaces that the target object implements. CGLIB creates, at run time, a subclass that extends the target class and uses it as the proxy. Spring Boot uses CGLIB by default. Because it works by inheritance, it cannot be applied to final classes or to final or private methods.
@Async
This annotation makes a method run in a separate thread pool, not in the thread that called it. The caller does not wait for the result and moves on right away. It is proxy-based, so it does not apply when the method is called from inside the same class, and because the method runs in another thread, the caller's transaction does not carry over.
@Transactional
This annotation is placed on a method or a class so that its scope runs as one transaction. The proxy starts a transaction before the method runs, commits if the method ends normally, and rolls back according to the rules if an exception occurs. You do not have to write the code that starts and ends the transaction yourself, so this is called a declarative transaction.

Try these

Running with the default settings

  1. When the application starts, Spring finds a bean that has a method annotated with @Transactional and registers, as the bean, a proxy that receives the calls in place of the original object. This proxy is what other beans receive by injection. A bean with no such method is registered as the original object itself.
  2. The controller calls orderService.placeOrder(). The proxy receives the call, but placeOrder() has no annotation, so the proxy passes the call on to the real object as it is.
  3. Inside the real object, the code calls saveAll(). This call is this.saveAll(). Here, this is the real object itself, not the proxy, so the call does not go through the proxy.
  4. No proxy is there to start a transaction. Order A is inserted with INSERT. There is no transaction, so this INSERT is committed as soon as it runs (auto-commit).
  5. Order B is inserted with INSERT. It is also committed right away.
  6. An exception is thrown, but there is no transaction to roll back. Orders A and B, which are already committed, stay in the DB as they are. The data of a failed operation is left half saved.

Result of this run

Code

@Service class OrderService {
    public void placeOrder() {                 // called by the controller
        saveAll();                             // same as this.saveAll()
    }

    @Transactional
    public void saveAll() {
        orderDao.insert(a);
        orderDao.insert(b);
        throw new IllegalStateException();   // fails partway
    }
}

Assumptions of this model

This lab is based on proxy-based AOP, which is the default in Spring (with AspectJ weaving, the annotations can also be applied to self-invocation, but that is uncommon). For @Async, @EnableAsync is assumed to be turned on. That each INSERT is committed right away when there is no transaction follows the auto-commit default of a JDBC connection. All 10 combinations were run and confirmed with @Transactional and @Async beans on a real Spring (Spring Framework 7.0.9). Only in calls that went through the proxy was a transaction open inside the method and no rows were left after the exception, and with self-invocation and with the private method the two rows stayed as they were. For @Async too, only the calls that went through the proxy ran on a separate thread (task-1), and with self-invocation the method ran to the end on the calling thread (the 3 seconds of sending the mail were shortened to 0.3 seconds for the test).

Previous lab: Bean lifecycle callbacks · Next lab: AOP: advice and pointcuts

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these