Spring puzzler: transactional @PostConstruct methods
Introduction
Today we’ll be looking at a Spring
puzzler - transactional @PostContruct
methods. Though it’s not a commonly used thing,
it can be useful to know some limitations of the Spring’s declarative transaction management approach.
@PostConstruct
methods
The @PostConstruct
are called automatically by Spring
after all of the bean’s dependencies were injected. Let’s look
at an example:
|
|
We have a MovieService
which is a Spring
bean, and it used field-injection to get a dependency - the EntityManager
. The question is,
when is our spring bean fully-initialized and ready to be used?
Usually we consider that after calling the constructor, the instantiated object is in the right state so it can be safely used. Let’s look at the logs to see if that’s the case:
2022-05-15 16:49:36.432 DEBUG 1451865 --- [main] i.e.spring.tx.management.MovieService: entityManager: null
2022-05-15 16:49:36.447 DEBUG 1451865 --- [main] i.e.spring.tx.management.MovieService: entityManager: Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@79ab97fd]
As we can see from the logs, when the MovieService
constructor is executing, the entityManager
field still has the value of null
. The @PostConstruct
methods
come to the rescue. They’re invoked after all of the bean’s dependencies were set, no matter the injection type used: constructor, field or setter.
Our logs prove that that’s the case, the field entityManager
is no longer null
when the @PostConstruct
method was called.
The puzzler
What will happen if we’ll slightly modify our previous example and try to persist a JPA
entity? Here are the options:
- The movie entity will be successfully persisted to the database
- The
init
method won’t be called -
BeanCreationException
will be thrown -
TransactionRequiredException
will be thrown
Take a wild guess :)
|
|
The right answer is:
- The movie entity will be successfully persisted to the database
- The
init
method won’t be called -
BeanCreationException
will be thrown -
TransactionRequiredException
will be thrown
Actually an exception will be thrown, specifically BeanCreationException
with a cause of TransactionRequiredException
. The BeanCreationException
exception
is thrown when a @PostConstruct
method throws an exception.
Here are the logs:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'movieService': Invocation of init method failed; nested exception is javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call
at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:160) ~[spring-beans-5.3.19.jar:5.3.19]
It’s worth stating that we deliberately used directly the EntityManager
, since it doesn’t create any transactions but its persist
method expects to be called with an active transaction. Using Spring-data-jpa
here will help, since Spring-data-jpa
creates
transactions as a last-resort, so using Spring-data-jpa
will fix the puzzler.
Explanation
But what really happened? In one of our previous blog posts we mentioned that Spring’s declarative transaction management approach (using the @Transactional
annotation)
is based-on proxies by default. Here’s a little refresher on how a proxy looks like:
Well, it turns out that at the time when the @PostConstruct
method is invoked, the proxy for our MovieService
was not created yet, so we can’t
use the @Transactional
annotation since there’s no proxy to intercept the init
method call and create a transaction for us. Very unfortunate, isn’t it?
How to fix it?
There are a couple of ways to fix this problem, let’s explore the one by one.
Using programmatic transaction management
Well, if during the @PostConstruct
method call the proxy is not ready, one option would be to get rid of declarative transaction
management and use the programmatic one, since it doesn’t rely on proxies, like this:
|
|
This approach is certainly more verbose, but it allows us to fix the issue. Let’s check the logs:
2022-05-16 07:02:29.688 DEBUG 1494469 --- [main] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-05-16 07:02:29.702 DEBUG 1494469 --- [main] o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(541713794<open>)] for JPA transaction
2022-05-16 07:02:29.704 DEBUG 1494469 --- [main] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@45b7c97f]
2022-05-16 07:02:29.704 DEBUG 1494469 --- [main] i.e.spring.tx.management.MovieService: entityManager: Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@74123110]
2022-05-16 07:02:29.715 DEBUG 1494469 --- [main] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2022-05-16 07:02:29.715 DEBUG 1494469 --- [main] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(541713794<open>)]
Hibernate:
insert
into
movies
(name, id)
values
(?, ?)
2022-05-16 07:02:29.728 DEBUG 1494469 --- [main] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(541713794<open>)] after transaction
As we can see, we do have a transaction and the JPA
entity was successfully inserted.
Listening to the ContextRefreshedEvent
event
Another option would be to not use the @PostConstruct
annotation, but listening to the ContextRefreshedEvent
event, which
is a spring event which is published after the Spring’s ApplicationContext
is refreshed. At this stage, the proxies for our
spring beans are guaranteed to be ready. It looks something like this:
|
|
In the example above, instead of the @PostConstruct
annotation, we’re using the @EventListener(ContextRefreshedEvent.class)
annotation.
Let’s check the logs to see if our JPA
entity was inserted properly:
2022-05-16 07:13:27.686 DEBUG 1495276 --- [main] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [inc.evil.spring.tx.management.MovieService.init]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-05-16 07:13:27.687 DEBUG 1495276 --- [main] o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(235386075<open>)] for JPA transaction
2022-05-16 07:13:27.688 DEBUG 1495276 --- [main] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@6f2d3391]
2022-05-16 07:13:27.693 DEBUG 1495276 --- [main] i.e.spring.tx.management.MovieService: entityManager: Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@69d103f0]
2022-05-16 07:13:27.698 DEBUG 1495276 --- [main] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2022-05-16 07:13:27.698 DEBUG 1495276 --- [main] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(235386075<open>)]
Hibernate:
insert
into
movies
(name, id)
values
(?, ?)
2022-05-16 07:13:27.705 DEBUG 1495276 --- [main] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(235386075<open>)] after transaction
As we can see, we do have a transaction this time and the JPA
entity was successfully inserted.
Proxy self-injection
This is the messiest and odd-looking solution that actually works. Let’s have a look:
|
|
In the example above, the MovieService
tries to @Autowire
itself. There are versions of the Spring framework (below 4.3
) in which this trick doesn’t work.
Starting with Spring Framework 4.3
, support for self-injection with the @Autowired
annotation was added, see release notes here.
To make it work, we need to add in the application.properties
file the following property (otherwise an UnsatisfiedDependencyException
will be thrown):
spring.main.allow-circular-references=true
When we do a “self-injection” with the @Autowired
annotation, what we actually get is our proxy! In this case, we
can try to call a @Transactional
method though the proxy and in this way we’ll get a transaction. For that we’ve added a new public
method
annotated with the @Transactional
annotation.
Let’s check the logs to see if it actually works:
2022-05-16 07:31:05.159 DEBUG 1526158 --- [main] i.e.spring.tx.management.MovieService: entityManager: null
2022-05-16 07:31:05.183 DEBUG 1526158 --- [main] i.e.spring.tx.management.MovieService: entityManager: Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@27c53c32]
2022-05-16 07:31:05.195 DEBUG 1526158 --- [main] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [inc.evil.spring.tx.management.MovieService.doInit]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
2022-05-16 07:31:05.208 DEBUG 1526158 --- [main] o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(266906347<open>)] for JPA transaction
2022-05-16 07:31:05.210 DEBUG 1526158 --- [main] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@42107318]
2022-05-16 07:31:05.228 DEBUG 1526158 --- [main] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
2022-05-16 07:31:05.228 DEBUG 1526158 --- [main] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(266906347<open>)]
Hibernate:
insert
into
movies
(name, id)
values
(?, ?)
2022-05-16 07:31:05.240 DEBUG 1526158 --- [main] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(266906347<open>)] after transaction
Indeed, our JPA
entity was successfully inserted into the database.
When doing the proxy self-injection, at the moment when the @PostConstruct
method is invoked, it is obvious that the proxy for
our MovieService
is ready (since @PostConstruct
methods are called after all of the bean’s dependencies we’re set).
What if we try to rewrite our example like this?:
|
|
Unfortunately it still won’t work because of the way the CommonAnnotationBeanPostProcessor
was implemented (well, to be more precise
its superclass - the InitDestroyAnnotationBeanPostProcessor
), and it is the one
which is calling the @PostConstruct
methods. This BeanPostProcessor
use the original bean instance when invoking init-methods, not the proxy!
|
|
Other pitfalls like this
When the @PostConstruct
methods are called, any proxy-based mechanisms (like @Async
, @Secured
, @Cacheable
) do not work, but
the fixes we’ve discussed in this blog post can be applicable.
For example, if we try to make the @PostConstruct
method asynchronous, like this:
|
|
It won’t work the way we expect it to. The MovieService.init()
will be called from the main
thread, not in a different one. See the logs below:
2022-05-16 08:36:36.779 DEBUG 1532607 --- [main] i.e.spring.tx.management.MovieService : Initializing MovieService
But if we try to apply the trick Listening to the ContextRefreshedEvent event, everything works as expected:
|
|
By looking at the logs we can see that this time, the MovieService.init()
method was called from the task-1
thread.
2022-05-16 08:38:55.242 DEBUG 1532809 --- [task-1] i.e.spring.tx.management.MovieService : Initializing MovieService
Conclusion
In this blog post we’ve looked at one limitation of Spring’s declarative transaction management - the fact that it can’t be used
in @PostConstruct
methods, since the proxy is not ready yet at that point in time.
We also looked at a couple of possible fixes to this problem, like using the programmatic approach,
doing proxy self-injection or listening to the ContextRefreshedEvent
.
Finally, we’ve discussed that this problem can be encountered when using other proxy-based mechanisms like @Async
, @Secured
or even @Cacheable
.
There are also a couple of more puzzlers regarding the @Transactional
annotation, we’ll take a look at them in another blog post.
The code can be found on GitHub