It' well-known that @Transactional or any other AOP around the method won't work if it was called from inside of the same class.

And explanation is clear and always made sense to me: the proxy wraps a real basic Java object. All the other clients of our class actually have a reference to proxy (injected by Spring), that's why AOP logic works. Inside of the same class we have a clean this without AOP, hence AOP does not work if you call AOP-suggesting method directly on this.

But at the same time there are @Configuration classes with @Bean methods for producing singletons which somehow get executed only once even if there is the code which explicitly invokes such method multiple times on this. Here is the practical example:

@Bean
String bean1() {
    System.out.println("Creating bean1 only once");
    return new StringBuilder("bean1").toString();
}

@Bean
StringHolder bean2() {
    return new StringHolder("bean2", bean1());
}

@Bean
StringHolder bean3() {
    return new StringHolder("bean3", bean1());
}

Both bean2() and bean3() explicitly call bean1(), but the "Creating bean1 only once" will be printed only once on context startup. How does it even work? The @Transactional/AOP examples linked above teach us that the framework's magic should not work when we call self methods.

The behavior of @Bean in @Configuration on the other hand is expected according to what Spring Core documentation says:

CGLIB proxying is the means by which invoking methods or fields within @Bean methods in @Configuration classes creates bean metadata references to collaborating objects. Such methods are not invoked with normal Java semantics but rather go through the container in order to provide the usual lifecycle management and proxying of Spring beans, even when referring to other beans through programmatic calls to @Bean methods

I've decided to check in debugger what is the reference to this inside of a @Bean method of @Configuraiton class and it turned out that this is not just an instance of MyConfigClass, but an instance of MyConfigClass$$SpringCGLIB$$0. So, this is a real evidence that this can somehow point to a CGLIB proxy and not just a basic instance of original class. If it can be achieved, why the same technique is not applied to instances of classes with @Transactional and other AOP?

My first clue was that @Transactional/AOP probably uses different types of proxies, so I've also inspected the reference to MyTransactionalService. Inside of a method of the service this points to instance of basic MyTransactionalService, but outside of the class DI container will inject a reference to MyTransactionalService$$SpringCGLIB$$0.

So, it looks like the same CGLIB approach is used for both cases: MyConfigClass and MyTransactionalService. But why methods of MyConfigClass see proxied object behind this ref, and at the same time methods of MyTransactionalService see bare non-proxied object?

1

There are 1 best solutions below

0
On

In general that is not a good idea to discuss decisions made by other developers, however I believe I can give some clues...

Let's consider following @Transactional bean:

@Transactional(propagation=PROPAGATION_REQUIRES_NEW)
void method1() {
    // do something
    method3()
    // do something
}

void method2() {
    // do something
    method3()
    // do something
}

@Transactional(propagation=PROPAGATION_REQUIRES_NEW)
void method3() {
    // do something
}

If spring were performing transformations similar to @Configuration classes (i.e. subclassing bean class and overriding @Transactional methods) that might be "desired" behaviour for #method2, but in case of #method1 we could start getting inconsistencies. And in order to overcome possible ambiguities you would need to write something like:

@Transactional(propagation=PROPAGATION_REQUIRES_NEW)
void method1() {
    // do something
    doMethod3()
    // do something
}

void method2() {
    // do something
    method3()
    // do something
}

@Transactional(propagation=PROPAGATION_REQUIRES_NEW)
void method3() {
    doMethod3()
}

void doMethod3() {
    // do something
}

Another example:

@Transactional
void method1() {
    // do something
    try {
       method2()
    } catch (Exception ex) {
       // ignore
    }
    // do something
}

@Transactional
void method2() {
    // do something
}

Now, exceptions raised in #method2 would mark transaction initiated in #method1 as rollback-only regardless of our attempts to catch those exceptions.

Both examples above demonstrate that @Transactional semantics also depends on caller side, and IMO that is much simpler to have a deal with current straightforward implementation, when all @Transactional annotations are basically ignored, rather than solving @Transactional puzzles every day.

FYI, self-injection IMO is not a good idea, the problem is when we do some tricks in code those tricks must be clearly visible and discoverable, it is much better to write something like:

class TransactionService {

   @Transactional
   public <T> T tx(Supplier<T> supplier) {
     return supplier.get();
   }

}