Configure Atomikos JTA with Spring Boot for Multi‑DataSource Transactions
This guide walks through setting up a Spring Boot project with JDK 8, Atomikos, and JPA to manage two MySQL data sources, configure JTA transaction handling, define entity managers, services, and a controller, and demonstrates successful and failing transaction scenarios with code snippets and screenshots.
Environment
JDK 8+, Atomikos 2.4.12, Spring Boot with JPA.
Dependencies (pom.xml)
<code><properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies></code>Application Configuration (application.yml)
<code>server:
port: 9101
---
spring:
application:
name: atomikos
---
spring:
jta:
enabled: true
datasource:
account:
resourceName: accountResource
dsClassName: com.mysql.cj.jdbc.MysqlXADataSource
url: jdbc:mysql://localhost:3306/account?serverTimezone=GMT+8&useSSL=false
username: root
password: 123123
storage:
resourceName: storageResource
dsClassName: com.mysql.cj.jdbc.MysqlXADataSource
url: jdbc:mysql://localhost:3306/storage?serverTimezone=GMT+8&useSSL=false
username: root
password: 123123
---
spring:
jpa:
generateDdl: false
hibernate:
ddlAuto: update
openInView: true
show-sql: true
properties:
hibernate:
physical_naming_strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
dialect: org.hibernate.dialect.MySQL5Dialect
transaction:
jta:
platform: com.pack.jpa.config.AtomikosJtaPlatform
javax:
persistence:
transactionType: JTA
---
debug: false</code>If physical_naming_strategy is not configured, column names may not match the expected naming convention.
Data Source Property Classes
<code>@Data
public class BaseDataSourceProperties {
private String resourceName;
private String dsClassName;
private String driver;
private String url;
private String username;
private String password;
}</code> <code>@Component
@ConfigurationProperties(prefix = "spring.datasource.account")
public class AccountDataSourceProperties extends BaseDataSourceProperties {}
</code> <code>@Component
@ConfigurationProperties(prefix = "spring.datasource.storage")
public class StorageDataSourceProperties extends BaseDataSourceProperties {}
</code>DataSource Beans
<code>@Configuration
public class DataSourceConfig {
@Bean(name = "accountDataSource", initMethod = "init", destroyMethod = "close")
@Primary
public DataSource accountDataSource(AccountDataSourceProperties props) {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName(props.getResourceName());
ds.setXaDataSourceClassName(props.getDsClassName());
Properties xaProperties = new Properties();
xaProperties.setProperty("url", props.getUrl());
xaProperties.setProperty("user", props.getUsername());
xaProperties.setProperty("password", props.getPassword());
ds.setXaProperties(xaProperties);
ds.setMinPoolSize(10);
ds.setMaxPoolSize(10);
ds.setBorrowConnectionTimeout(30);
ds.setMaxLifetime(60);
ds.setMaintenanceInterval(60);
return ds;
}
@Bean(name = "storageDataSource", initMethod = "init", destroyMethod = "close")
public DataSource storageDataSource(StorageDataSourceProperties props) {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
ds.setUniqueResourceName(props.getResourceName());
ds.setXaDataSourceClassName(props.getDsClassName());
Properties xaProperties = new Properties();
xaProperties.setProperty("url", props.getUrl());
xaProperties.setProperty("user", props.getUsername());
xaProperties.setProperty("password", props.getPassword());
ds.setXaProperties(xaProperties);
ds.setMinPoolSize(10);
ds.setMaxPoolSize(10);
ds.setBorrowConnectionTimeout(30);
ds.setMaxLifetime(60);
ds.setMaintenanceInterval(60);
return ds;
}
}
</code>JPA Entity Manager Configuration
<code>public class EntityManagerFactoryConfig {
@Configuration
@EnableJpaRepositories(
basePackages = {"com.pack.account.repository"},
entityManagerFactoryRef = "accountEntityManagerFactory",
transactionManagerRef = "transactionManager"
)
@DependsOn("transactionManager")
static class AccountEntityManagerFactory {
@Resource(name = "accountDataSource")
private DataSource accountDataSource;
@Resource
private JpaProperties props;
private String accountDomainPkg = "com.pack.account.domain";
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean accountEntityManagerFactory(EntityManagerFactoryBuilder builder) {
return builder
.dataSource(accountDataSource)
.packages(accountDomainPkg)
.persistenceUnit("account")
.properties(props.getProperties())
.build();
}
}
@Configuration
@EnableJpaRepositories(
basePackages = {"com.pack.storage.repository"},
entityManagerFactoryRef = "storageEntityManagerFactory",
transactionManagerRef = "transactionManager"
)
@DependsOn("transactionManager")
static class StorageEntityManagerFactory {
@Resource(name = "storageDataSource")
private DataSource storageDataSource;
@Resource
private JpaProperties props;
private String storageDomainPkg = "com.pack.storage.domain";
@Bean
public LocalContainerEntityManagerFactoryBean storageEntityManagerFactory(EntityManagerFactoryBuilder builder) {
return builder
.dataSource(storageDataSource)
.packages(storageDomainPkg)
.persistenceUnit("storage")
.properties(props.getProperties())
.build();
}
}
}
</code>Transaction Management
<code>public class AtomikosJtaPlatform extends AbstractJtaPlatform {
private static final long serialVersionUID = 1L;
public static TransactionManager transactionManager;
public static UserTransaction transaction;
@Override
protected TransactionManager locateTransactionManager() {
return transactionManager;
}
@Override
protected UserTransaction locateUserTransaction() {
return transaction;
}
}
</code> <code>@Configuration
public class TxConfig {
@Bean(name = "userTransaction")
public UserTransaction userTransaction() throws Throwable {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}
@Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
public TransactionManager atomikosTransactionManager() throws Throwable {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}
@Bean(name = "transactionManager")
@DependsOn({"userTransaction", "atomikosTransactionManager"})
public PlatformTransactionManager transactionManager() throws Throwable {
UserTransaction userTransaction = userTransaction();
TransactionManager atomikosTransactionManager = atomikosTransactionManager();
return new JtaTransactionManager(userTransaction, atomikosTransactionManager);
}
}
</code>The AtomikosJtaPlatform class is the custom JTA platform referenced in the application.yml configuration.
Entity Classes
<code>@Entity
@Table(name = "t_account")
public class Account {
@Id
private Long id;
private String userId;
private BigDecimal money;
}
</code> <code>@Entity
@Table(name = "t_storage")
public class Storage {
@Id
private Long id;
private String commodityCode;
private Integer count;
}
</code>Repositories and Services
<code>public interface AccountRepository extends JpaRepository<Account, Long>, JpaSpecificationExecutor<Account> {}
public interface StorageRepository extends JpaRepository<Storage, Long>, JpaSpecificationExecutor<Storage> {}
</code> <code>@Service
public class AccountService {
@Resource
private AccountRepository accountRepository;
@Transactional
public void saveAccount(Account account) {
accountRepository.save(account);
}
}
</code> <code>@Service
public class StorageService {
@Resource
private StorageRepository storageRepository;
@Transactional
public void saveStorage(Storage storage) {
storageRepository.save(storage);
}
}
</code> <code>@Service
public class OperatorService {
@Resource
private AccountService accountService;
@Resource
private StorageService storageService;
@Transactional
public void save(Account account, Storage storage) {
accountService.saveAccount(account);
if (storage.getCount() == 0) {
throw new RuntimeException("库存数量错误0");
}
storageService.saveStorage(storage);
if (storage.getCount() == -1) {
throw new RuntimeException("库存数量错误-1");
}
}
}
</code>The @Transactional annotation on OperatorService.save ensures that if any exception occurs, the whole transaction is rolled back.
Controller and Test Cases
<code>@RestController
@RequestMapping("/oc")
public class OperatorController {
@Resource
private OperatorService os;
@PostMapping("/save")
public Object save(@RequestBody OperatorDTO dto) {
os.save(dto.getAccount(), dto.getStorage());
return "1";
}
}
</code>Successful execution inserts both records; failure (e.g., setting count to 0 or -1) triggers an exception and rolls back the transaction.
When the transaction fails, no data is persisted to the database.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.