Spring-boot with MyBatis doesn't rollback transactions - spring-boot

I've got a Spring Boot (version 2.1.8.RELEASE) web application (deployed inside a Wildfly 9 application container), with MyBatis, being auto-configured using the Spring Boot starter, but when using the #transactional annotation, the statements are always committed, even when they should be rolled back. My pom.xml fragment looks like this:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
I've got the following lines in my application.properties:
spring.datasource.url=jdbc:sqlserver://my.server.com:1433;databaseName=MyDatabase
spring.datasource.username=myUsername
spring.datasource.password=myPassword
mybatis.config-location=classpath:mybatis-config.xml
And this is my mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
<typeAliases>
<package name="my.package.model"/>
</typeAliases>
<mappers>
...
</mappers>
</configuration>
My application initialiser class looks like this:
#SpringBootApplication
#EnableTransactionManagement
#ComponentScan("my.packag e")
public class ServletInitializer extends SpringBootServletInitializer
{
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder)
{
// some config here
return builder.sources(ServletInitializer.class);
} // end method configure()
#Override
public void onStartup(ServletContext servletContext) throws ServletException
{
super.onStartup(servletContext);
// some config here
} // end method onStartup()
// some other beans here
public static void main(String[] args)
{
SpringApplication.run(ServletInitializer.class, args);
}
} // end class ServletInitializer
I've got a controller, which isn't part of the transaction but which autowires in a service layer bean:
#Controller
public class DataMigrationController
{
#AutoWired private MyService service;
#GetMapping("/path")
public #ResponseBody Boolean something(Model model, HttpSession session)
{
service.doTask();
return true;
}
}
And my service class is like this:
#Service
public class MyService
{
#AutoWired private MyMapper mapper;
#Transactional(rollbackFor=Throwable.class)
public void doTask()
{
Person p= new Person();
p.setPersonID("999999");
p.setSurname("TEST");
p.setForename1("TEST");
p.setTitle("Mr");
mapper.insertPerson(p);
throw new RuntimeException();
}
}
I would expect the transaction to be rolled back because of the RuntimeException being thrown at the end of the doTask() method but when I check the database, the row is present. I've also tried using TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); instead of throwing the exception, but I get the same result.
I'm using the transaction debug class suggested by this blog post which tells me that there's no transaction in the controller (which I would expect) and there is one in the service class. But for some reason, the transaction just isn't rolling back.
Can anyone please advise?

I found a reason that commit a transaction. The Spring Boot use JTA transaction management under Java EE(Jakarta EE) environment by default settings. But the DataSource that created via Spring Boot can not join it.
You can select a solution as follows:
Disable the JTA transaction management
Use a transactional DataSource managed by Wildfly
How to disable JTA transaction management
You can disable the JTA transaction management as follow:
spring.jta.enabled=false
For details, see https://docs.spring.io/spring-boot/docs/2.1.9.RELEASE/reference/htmlsingle/#boot-features-jta.
How to use a DataSource managed by Wildfly
You can use the a DataSource managed by Wildfly.
spring.datasource.jndi-name=java:jboss/datasources/demo
For details, see https://docs.spring.io/spring-boot/docs/2.1.9.RELEASE/reference/htmlsingle/#boot-features-connecting-to-a-jndi-datasource
Note: How to configure a DataSource (need to enable JTA) on Wildfly, see https://docs.jboss.org/author/display/WFLY9/DataSource+configuration?_sscc=t.

Related

Should I use #Transactional using #EnableTransactionManagement?

I've got a Spring Boot application. Everything works fine. I'm just trying to understand how does transaction manager work, because I have suspicion about my application. In particular, I'm a little confused about annotations.
Here is Application.java (main class):
#SpringBootApplication(exclude = ActiveMQAutoConfiguration.class)
#EnableScheduling
public class Application extends SpringBootServletInitializer
{
#Override
protected SpringApplicationBuilder configure (SpringApplicationBuilder builder) {
return builder.sources(Application.class);
}
public static void main(String[] args)
{
SpringApplication.run(Application.class, args);
}
}
config class DataConfig.java looks like:
#Configuration
#EnableTransactionManagement
#ComponentScan("com.pr.hotel")
#EnableJpaRepositories("com.pr.hotel")
#PropertySource("classpath:application.properties")
public class DataConfig
{
// code
}
I'm worry about #EnableTransactionManagement. What exactly does this annotation mean? Should I use #Transactional in this case (I don't)?
#EnableTransactionManagement does exactly what it says:
Enables Spring's annotation-driven transaction management capability,
similar to the support found in Spring's XML namespace.
Yes, you should still use #Transactional annotation on methods that you want to wrap in a transaction. In the following example the result of saveSomething() wouldn't be applied if maybethrowaneException() threw an exception. Be careful to use org.springframework.transaction.annotation.Transactional and not javax.transaction.Transactional.
The #Transactional annotation tells Spring to control when the data are flushed to the database (typically once the method successfully completes). Without the annotation the data would be flushed immediately.
It's a mechanism to prevent incomplete changes being written to the database when something goes wrong. Further reading: https://dzone.com/articles/how-does-spring-transactional
#Service
public class DataTransformer() {
#Transactional
public void doETL() throws Exception {
loadSomeEntities();
saveSomething();
maybethrowanException();
saveSomethingElse();
}
}

Dependency injection into Logback Appenders with Spring Boot

I'm using Spring Boot 1.5.2 with Logback, which is configured using a logback-spring.xml. There, I define an appender of a custom type (subclass of RollingFileAppender) and would like to get a pair of beans injected.
Is this possible? I naively tried annotating the appender #Component etc. but as it is created by Logback/Joran, it of course doesn't work. Is there a trick I can apply?
If not possible, what would be the canonical way of achieving my goal (inserting beans from the application context into an appender)?
As mentioned also in the question, by default, Logback instantiates and manages the lifecycle of different logging components (appenders, etc) itself. It knows nothing of Spring. And, Logback typically configures itself way before Spring is started (as Spring also uses it for logging).
So, you cannot really use Spring to configure an instance of FileAppender (or some other rather fundamental appender) and then inject that into Logback.
However, in case your appender is not truly fundamental (or you are happy to ignore logging events during Spring Boot startup), you can follow the "simple" approach below. In case you would like to capture all events (including the ones during startup), keep on reading.
Simple approach (loses events during startup)
Create your appender as a Spring component:
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;
#Component
public class LogbackCustomAppender extends UnsynchronizedAppenderBase<ILoggingEvent> implements SmartLifecycle {
#Override
protected void append(ILoggingEvent event) {
// TODO handle event here
}
#Override
public boolean isRunning() {
return isStarted();
}
}
As you can see, it is annotated with #Component so that Spring will pick it up during classpath scanning. Also, it implements SmartLifecycle so that Spring will call Logback Lifecycle interface methods (luckily, start() and stop() methods have exactly the same signature, so we only need to implement isRunning() which will delegate to Logback isStarted()).
Now, by the end of Spring application context startup, we can retrieve the fully initialized LogbackCustomAppender instance. But Logback is blissfully unaware of this appender, so we need to register it with Logback.
One way of doing this is within your Spring Boot Application class:
#SpringBootApplication
#ComponentScan(basePackages = {"net.my.app"})
public class CustomApplication {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(CustomApplication.class, args);
context.start();
addCustomAppender(context, (LoggerContext) LoggerFactory.getILoggerFactory());
}
private static void addCustomAppender(ConfigurableApplicationContext context, LoggerContext loggerContext) {
LogbackErrorCollector customAppender = context.getBean(LogbackCustomAppender.class);
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(customAppender);
}
}
No need to change anything in your Logback configuration file.
More complicated approach (captures all events)
As mentioned above, you might be interested in not losing the events logged during Spring Boot startup.
For this, you could implement a placeholder appender (that would buffer startup events internally):
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import java.util.ArrayList;
public class BufferingAppenderWrapper<E> extends UnsynchronizedAppenderBase<E> {
private final ArrayList<E> eventBuffer = new ArrayList<>(1024);
private Appender<E> delegate;
#Override
protected void append(E event) {
synchronized (eventBuffer) {
if (delegate != null) {
delegate.doAppend(event);
}
else {
eventBuffer.add(event);
}
}
}
public void setDelegateAndReplayBuffer(Appender<E> delegate) {
synchronized (eventBuffer) {
this.delegate = delegate;
for (E event : this.eventBuffer) {
delegate.doAppend(event);
}
this.eventBuffer.clear();
}
}
}
We register that appender with Logback the usual way (e.g. logback.xml):
<appender name="DELEGATE" class="my.app.BufferingAppenderWrapper" />
<root level="INFO">
<appender-ref ref="DELEGATE" />
</root>
After Spring has started, look that appender up by name and register your Spring-configured appender with the placeholder (flushing the buffered events in the process):
#SpringBootApplication
#ComponentScan(basePackages = {"net.my.app"})
public class CustomApplication {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(CustomApplication.class, args);
context.start();
addCustomAppender(context, (LoggerContext) LoggerFactory.getILoggerFactory());
}
private static void addCustomAppender(ConfigurableApplicationContext context, LoggerContext loggerContext) {
LogbackErrorCollector customAppender = context.getBean(LogbackCustomAppender.class);
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
BufferingAppenderWrapper<ILoggingEvent> delegate = (BufferingAppenderWrapper<ILoggingEvent>) rootLogger.getAppender("DELEGATE");
delegate.setDelegateAndReplayBuffer(customAppender);
}
}
LogbackCustomAppender stays the same.
It isn't possible to do what you are trying to do. Logback is initialised before the application context is created so there's nothing to perform the dependency injection.
Perhaps you could ask another question describing what you'd like your appender to be able to do? There may be a solution that doesn't involve injecting Spring-managed beans into it.
Using logback-extensions you can create your appenders in a spring application context file or in a spring config factory.
Try defining a bean like this and calling the static getBean method on it, instead of using dependency injection:
#Component
public class BeanFinderGeneral implements ApplicationContextAware {
private static ApplicationContext applicationContext;
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
#Override
public void setApplicationContext(ApplicationContext pApplicationContext) throws BeansException {
applicationContext = pApplicationContext;
}
}
In Spring boot you can write a configuration class and create a Bean of your logback class as below:
#Component
#Configuration
public class LogBackObjectBuilder {
#Bean
public RollingFileAppender myRollingFileAppender() {
return new YOUR-SUB-CLASS-OF-RollingFileAppender();
}
}
Just having this class scanned by spring will cause this Bean to be created and injected in the context.
I hope I understood your question right. You want your custom appender to be injected in the application context.

Spring Boot: Using Tomcat context parameters in logback configuration

I have several instances of a Spring Boot app deployed in a unique Tomcat.
Each app is configured with a context.xml file which contains a customer code
<Context path="/myApp1" reloadable="false">
<Parameter name="CUSTOMER_CODE" value="CUSTOMER1" />
</Context>
I wish that each customer has a separate log based on code defined in context.xml.
Unfortunately this config doesn't work in my logback-config.xml:
<property name="LOG_FILE" value="${ROOT_LOG}/${CUSTOMER_CODE}/myApp.log}"/>
A folder CUSTOMER_CODE_IS_UNDEFINED is created in "ROOT_LOG" directory. "ROOT_LOG" is provided by a system property.
Is there any way to make this logback configuration working?
The use of properties defined in application.properties works well (I renamed my logback.xml to logback-spring.xml). It seems to me that Spring boot does not set Tomcat context parameters in Environnement before initialize logging. Any idea for a workaround? Thanks.
I finally found a solution to get my customer code available in Spring Environment bean before logging initialization. Not very pretty but it's working:
#Configuration
#EnableAutoConfiguration
#ComponentScan
public class Application extends SpringBootServletInitializer {
public static String APP_CUSTOMER_CODE;
/**
* Initialization of Spring Boot App with context param customer code
*/
#Override
protected SpringApplicationBuilder configure(final SpringApplicationBuilder builder) {
final Map<String, Object> initProps = new HashMap<>();
initProps.put("CUSTOMER_CODE", APP_CUSTOMER_CODE);
return builder.properties(initProps).sources(Application.class);
}
/**
* Method called before Spring Initialization
*/
#Override
public void onStartup(final ServletContext servletContext) throws ServletException {
APP_CUSTOMER_CODE = servletContext.getInitParameter("CUSTOMER_CODE");
super.onStartup(servletContext);
}
}
In addidtion, i must declare a springProperty tag in logback-spring.xml to use the variable:
<springProperty name="CUSTOMER_CODE" source="CUSTOMER_CODE"/>

Ehcache local transactions with Spring #Transactional

I'm trying to setup a transactional ehcache, making use of Spring #Cacheable and #Transactional.
My caches work fine with #Cacheable, but as soon as i setup my cache to use a local transaction:
<cache name="currencyCodeMaps" maxElementsInMemory="100" overflowToDisk="false" timeToIdleSeconds="5" timeToLiveSeconds="600" memoryStoreEvictionPolicy="LRU" transactionalMode="local"/>
When I access the cache i get error:
net.sf.ehcache.transaction.TransactionException: transaction not started
even though the same method is annotated #Transactional.
My Spring transaction manager is:
org.springframework.orm.jpa.JpaTransactionManager
The ehcache documentation says local transactions are controlled explicitly:
Local transactions are not controlled by a Transaction Manager.
Instead there is an explicit API where a reference is obtained to a
TransactionController for the CacheManager using
cacheManager.getTransactionController() and the steps in the
transaction are called explicitly
But this will be hard, as I want to sync my ehcache transactions with DB transactions, and DB transactions are controlled by #Transactional.
Is there a way to get local Ehcache transactions to work with Spring #Transactional?
Yes, there is a way to achieve you goal.
Because you have 2 transactional resources (JTA and Ehcache) and do not use JTA you have to use compound transaction manager likeorg.springframework.data.transaction.ChainedTransactionManager from spring-data project
#Bean
public PlatformTransactionManager transactionManager() {
return new ChainedTransactionManager(ehcacheTransactionManager(), jpaTransactionManager());
}
#Bean
public EhcacheTransactionManager ehcacheTransactionManager() {
return new EhcacheTransactionManager(ehcacheManager().getTransactionController());
}
#Bean
public PlatformTransactionManager jpaTransactionManager() {
return new JpaTransactionManager(entityManagerFactory());
}
You need to specify which transaction manager should be use by default:
#Configuration
public class Configuration implements TransactionManagementConfigurer {
...
#Override
public PlatformTransactionManager annotationDrivenTransactionManager() {
return transactionManager();
}
...
}
EhcacheTransactionManager implementation
import net.sf.ehcache.TransactionController;
import net.sf.ehcache.transaction.local.LocalTransactionContext;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
public class EhcacheTransactionManager extends AbstractPlatformTransactionManager {
private TransactionController transactionController;
public EhcacheTransactionManager(TransactionController transactionController) {
this.transactionController = transactionController;
}
#Override
protected Object doGetTransaction() throws TransactionException {
return new EhcacheTransactionObject(transactionController.getCurrentTransactionContext());
}
#Override
protected void doBegin(Object o, TransactionDefinition transactionDefinition) throws TransactionException {
int timeout = transactionDefinition.getTimeout();
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
transactionController.begin(timeout);
} else {
transactionController.begin();
}
}
#Override
protected void doCommit(DefaultTransactionStatus defaultTransactionStatus) throws TransactionException {
transactionController.commit();
}
#Override
protected void doRollback(DefaultTransactionStatus defaultTransactionStatus) throws TransactionException {
transactionController.rollback();
}
public class EhcacheTransactionObject {
private LocalTransactionContext currentTransactionContext;
public EhcacheTransactionObject(LocalTransactionContext currentTransactionContext) {
this.currentTransactionContext = currentTransactionContext;
}
}
}
source code and test case can be found here
This solution has a significant drawback transaction coordinator of ehcache does not support suspend/resume operations so inner transactions (PROPAGATION_REQUIRES_NEW) are not possible. That is why I had to find another one.
Another option is not to use local ehcache transactions at all and use org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager#setTransactionAware which decorates caches to postpone operations until the transaction end. But it has following drawbacks:
Evicted keys stay accessible inside transaction until transaction commit
putIfAbsent operation is not postponed
It was a problem for me, so I implemented this functionality in different way. Check 'me.qnox.springframework.cache.tx.TxAwareCacheManagerProxy', there problems described above was solved, in the same repository
You do not want local transactions, you want XA transactions, which are supported by Ehcache.
Have a look at the documentation for Ehcache 2.10.x or Ehcache 2.8.x.

Custom platform transaction manager in spring

I'm trying to implement custom transactional cache in a spring boot application. I've created my own implementation of AbstractPlatformTransactionManager and some unit tests, which show transactions are working as expected. However the real application ignores my transaction manager - it`s methods are never called. What I'm doing wrong? Thank you.
Transaction manager implementation:
#Component
public class CacheTransactionManager extends AbstractPlatformTransactionManager{
#Override
protected Object doGetTransaction() throws TransactionException {
...
}
...
}
Cache transaction configuration:
#Configuration
#EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class CacheTransactionConfiguration {
#Bean(name = "cacheTransactionManager")
public PlatformTransactionManager cacheTransactionManager() {
return new CacheTransactionManager();
}
}
Custom transactional annotation (I've tried also without this, but no difference):
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Transactional(value = "cacheTransactionManager", rollbackFor = Exception.class)
public #interface CacheTransactional {
}
Cache service:
#Component
public class CacheService {
#CacheTransactional
public void add(Object o){
...
}
}
Working JUnit test:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = TestApplication.class)
#Configuration
#EntityScan(...)
#IntegrationTest
#TransactionConfiguration(defaultRollback = false)
public class CacheTransactionManagerTest {
#Autowired
private CacheService cacheService;
#Test
#CacheTransactional
public void transactionTest(){
cacheService.add(new Object());
}
}
Not working wicket application main class (ignores cacheTransactionManager):
#Configuration("MyApplication")
#EnableAutoConfiguration
#EntityScan(...)
#EnableJpaRepositories(...)
#EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
#ComponentScan(...)
#ImportResource({...})
public class MyApplication extends AuthenticatedWebApplication {
...
}
My env: Java 8, Spring Boot 1.2.1, Spring 4.1.4, Spring data JPA 1.7.2, Hibernate 4.3.7, Apache Tomcat 8.0.15, Wicket 6.17.0
I found out some new facts:
when I remove AdviceMode.ASPECTJ from #EnableTransactionManagement on CacheTransactionConfiguration, transactions begin to work, but propagation of transaction is ignored - nested call from one #CacheTransactional method to another #CacheTransactional methods always creates new transaction. Same behavior in JUnit test and real application.
when AdviceMode.ASPECTJ is on CacheTransactionConfiguration setted, but I remove #CacheTransactional annotation from junit test, transaction stops working also in juint (in test body is a #CacheTransaction method called, so there should be a transaction created).
application log contains this entry:
o.s.c.a.ConfigurationClassBeanDefinitionReader isOverriddenByExistingDefinition:290 - Skipping bean definition for [BeanMethod:name=cacheTransactionManager,declaringClass=x.y.z.CacheTransactionConfiguration]: a definition for bean 'cacheTransactionManager' already exists. This top-level bean definition is considered as an override.
So I can get this working, but without propagation...
For propagation, you need to tell Spring's #Transactional what propagation mode to apply. You can define several tx annotations, each inherit from #Transactional, but with a different propagation mode.

Resources