StackOverflow.com, the question and answer website
frequented by programmers is
full
of
posts
about the @Transactional
annotation provided by the
Spring Framework "not working". This
blog post attempts to summarize the various reasons why the
said annotation "does not work" at times, along with relevant
"solutions".
1. Have transactions been enabled?
A
common
assumption among new users of the Spring framework is that the
@Transactional
annotation "just works out of the box",
that is, simply annotating a class or a method with the said
annotation is sufficient to gain transactional semantics.
Alas, that is not the case. As explained in the
official documentation,
declarative transaction management needs to be enabled explicitly
for a Spring application, for transaction semantics to be applied
automatically using the @Transactional
annotation.
1.1. Enabling transactions in Spring Boot
When using Spring Boot with auto-configuration enabled using
@SpringBootApplication
, transaction management is
enabled automatically, and no additional configuration is required.
1.2. Enabling transactions with Java configuration
Transaction management can be enabled with Java configuration
by annotating the configuration class (the one annotated with
@Configuration
) with
@EnableTransactionManagement
, as shown below:
@Configuration
@EnableTransactionManagement
public class ApplicationConfiguration {
@Bean
public PlatformTransactionManager transactionManager() {
// Return an appropriate subclass of PlatformTransactionManager.
}
...
}
1.3. Enabling transactions with XML configuration
In order to enable transaction management with XML configuration,
the line <transaction:annotation-driven />
must be added to the root context configuration for the
application, as shown below:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:transaction="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="transactionManager" class="[a subclass of PlatformTransactionManager]" />
<transaction:annotation-driven />
...
</beans>
2. Have transactions been enabled correctly?
A point
oft-missed
about web applications developed using the Spring framework
(other than Spring Boot) is that such applications have two
Spring contexts - the servlet context, and the
root context. The snippet below from a
web.xml
file makes this distinction clear:
<web-app>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:root-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>dispatcher-servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:servlet-context.xml</<param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher-servlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
The bottom part of the file (comprising of the servlet
and servlet-mapping
sections) creates and initializes
the servlet context.
The top part of the file (comprising of the
context-param
and listener
sections)
creates and initializes the root context. It also attaches
the root context to the servlet context so that Spring beans
invoked by the dispatcher servlet can access other Spring beans
outside the web layer as well.
If the web layer should control transaction boundaries,
transaction management should be enabled through the servlet
context. This would be the case for example when controller
classes and/or methods need to be/are annotated with
@Transactional
.
If on the other hand the transaction boundaries lie in layers below the web layer, transaction management should be enabled through the root context.
3. The tale of (two) @Transactional
(s)
The Spring @Transactional
annotation can be found
in the spring-tx.jar
file. When using a build tool
such as Maven or Gradle, this file also imports the
javax.transaction-api.jar
file. Unfortunately, this
later file also contains a @Transactional
annotation
(@javax.transaction.Transactional
to be precise).
The recent versions of the Spring framework allow using either of these two annotations for declarative transaction management. However, older versions of the framework do not support the later annotation. Developers using auto-import feature of code editors may find their editor importing the later annotation, which Spring framework may not recognize.
Developers must therefore make sure to use
@org.springframework.transaction.annotation.Transactional
to enforce declarative transaction management.
4. Internal method calls
When declarative transaction management is enabled using
(just) @EnableTransactionManagement
or
<transaction:annotation-driven />
, Spring
generates proxies for the classes for which transaction
management is required (annotated with
@Transactional
). As explained in the
official documentation,
the default proxies can only intercept method calls coming
from outside the implementation class. This means, transaction
semantics will not work for the following case:
@Component
public class FooComponent {
public void bar() {
this.baz();
}
@Transactional
protected void baz() {
...
}
}
If such a case needs to be supported, consider using AspectJ
based proxies by including spring-aspectjs.jar
on
the classpath and enabling transaction management as
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
(with Java configuration) or
<transaction:annotation-driven mode="aspectj" />
(with XML configuration).
5. To inherit (or not!)
Consider the following Java code:
@Transactional
public interface FooService {
void bar();
}
@Component
public class FooServiceImpl implements FooService {
public void bar() {
...
}
}
Developers expect FooServiceImpl.bar()
to be
transactional. However, they will find this not to be case
when declarative transaction management is enabled using
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
or
<transaction:annotation-driven mode="aspectj" />
.
As explained in the
official documentation,
Java annotations are not inherited by classes from the
interfaces they implement. The consequence of this limitation
is that the implementing classes are wrapped in proxies that are
not transactional in nature (because the classes, and therefore
their proxies are not aware of the @Transactional
annotation).
The solution is to move the @Transactional
annotation from the interface to the implementing class.
6. To inherit (or not!), part 2
Consider the following Java code (similar to the one above):
public interface FooService {
void bar();
}
@Component
@Transactional
public class FooServiceImpl implements FooService {
public void bar() {
...
}
}
@Component
public class FooConsumer {
@Autowired
FooService fooService;
public void consume() {
fooService.bar();
}
}
It is intuitively reasonable to expect that a call to
FooConsumer.consume()
will in turn call
FooServiceImpl.bar()
, which would be invoked
in transactional context. However, this too may not work.
This is due to the fact that the default proxies are based
on the interfaces, not the implementing classes. In the case
above, the proxy is generated for FooService
,
not FooServiceImpl
.
The solution is to generate proxies for the implementing classes
instead of the interfaces, as
@EnableTransactionManagement(proxyTargetClass = true)
(with Java configuration) or
<transaction:annotation-driven proxy-target-class="true" />
(with XML configuration).
7. @Transactional
does not roll back upon exception
A common situation usually involves code similar to the following:
@Component
public class FooService {
@Transactional
public void bar() {
baz(1);
...
}
private void baz(int i) throws Exception {
if (i < 2) {
throw new Exception("i must not be less than 2.");
}
...
}
}
It is usually expected that the transaction started by
FooService.bar()
will get rolled back as soon as
FooService.baz()
is called, because the later method
throws an Exception
. However, as explained in the
official documentation,
the Spring infrastructure rolls a transaction back only in the
case of runtime, unchecked exceptions. This means,
@Transactional
is short for
@Transactional(rollbackFor = RuntimeException.class)
.
Therefore, changing throw new Exception(...)
to
throw new RuntimeException(...)
should mark the
transaction to roll back automatically.
Alternatively, changing @Transactional
to
@Transactional(rollbackFor = Exception.class)
should
also mark the transaction to roll back automatically, although
the former approach is preferable to this, as per the Spring
framework's philosophy of using only runtime exceptions.
8. @Transactional
does not roll back upon runtime exception
Yet another common problem faced by many developers is that of transactions not rolling back, despite following all the steps described above.
In most cases, the problem is one of the following.
a. Underlying data store does not support transactions
Quite a few data stores, most notably the MySQL ISAM engine, and older versions of MongoDB do not support transactions. Therefore, attempting to enlist such data stores in a transactional context will have no effect.
b. Operations involve more than one data store
Sometimes a transaction can span more than one data store, for example, two different database schemas, a database schema and a JMS queue, a JMS queue and an ERP system. In such cases, even though the individual data stores support transactions, users may find that an operation spanning multiple data stores is not atomic. This usually manifests itself as data getting saved to one data store but not the other.
For such cases, developers need to switch to XA transactions.