Introduce enforceReadOnly flag for JTA 2.1 read-only mode

Closes gh-35915
This commit is contained in:
Juergen Hoeller
2026-08-06 20:18:13 +02:00
parent 51c4539bb6
commit 7f01cd0a5b
4 changed files with 139 additions and 4 deletions
@@ -78,8 +78,8 @@ public class JtaTransactionAnnotationParser implements TransactionAnnotationPars
}
rbta.setRollbackRules(rollbackRules);
if (attributes.containsKey("readOnly")) { // JTA 2.1
rbta.setReadOnly(attributes.getBoolean("readOnly"));
if (attributes.containsKey("isReadOnly")) { // JTA 2.1
rbta.setReadOnly(attributes.getBoolean("isReadOnly"));
}
return rbta;
@@ -19,6 +19,8 @@ package org.springframework.transaction.jta;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Properties;
@@ -52,6 +54,8 @@ import org.springframework.transaction.support.AbstractPlatformTransactionManage
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -139,6 +143,10 @@ public class JtaTransactionManager extends AbstractPlatformTransactionManager
"java:comp/TransactionSynchronizationRegistry";
// JTA 2.1 UserTransaction#begin(boolean) method available?
private static final @Nullable Method beginWithReadOnlyMethod =
ClassUtils.getMethodIfAvailable(UserTransaction.class, "begin", boolean.class);
private transient JndiTemplate jndiTemplate = new JndiTemplate();
private transient @Nullable UserTransaction userTransaction;
@@ -165,6 +173,8 @@ public class JtaTransactionManager extends AbstractPlatformTransactionManager
private boolean allowCustomIsolationLevels = false;
private boolean enforceReadOnly = false;
/**
* Create a new JtaTransactionManager instance, to be configured as bean.
@@ -418,6 +428,20 @@ public class JtaTransactionManager extends AbstractPlatformTransactionManager
this.allowCustomIsolationLevels = allowCustomIsolationLevels;
}
/**
* Set whether a read-only transaction should be exposed to the JTA transaction manager,
* enforcing strict read-only access to resources as per the JTA 2.1 specification.
* <p>This requires JTA 2.1. The default is "false". By default, the read-only status
* of a Spring transaction is just exposed to transaction synchronization (for example,
* suppressing a Hibernate flush), while the JTA transaction itself will regularly commit.
* Turn this flag on if your transactional XA resources are known to support the JTA 2.1
* {@code ExtendedXAResource} SPI, operating in a read-only mode with eventual rollback.
* @since 7.1
*/
public void setEnforceReadOnly(boolean enforceReadOnly) {
this.enforceReadOnly = enforceReadOnly;
}
/**
* Initialize the UserTransaction as well as the TransactionManager handle.
@@ -858,7 +882,29 @@ public class JtaTransactionManager extends AbstractPlatformTransactionManager
applyIsolationLevel(txObject, definition.getIsolationLevel());
int timeout = determineTimeout(definition);
applyTimeout(txObject, timeout);
txObject.getUserTransaction().begin();
if (this.enforceReadOnly && definition.isReadOnly()) {
if (beginWithReadOnlyMethod == null) {
throw new NotSupportedException("enforceReadOnly requires JTA 2.1");
}
try {
beginWithReadOnlyMethod.invoke(txObject.getUserTransaction(), true);
}
catch (Exception ex) {
if (ex instanceof InvocationTargetException ite) {
if (ite.getTargetException() instanceof NotSupportedException nse) {
throw nse;
}
if (ite.getTargetException() instanceof SystemException se) {
throw se;
}
}
ReflectionUtils.handleReflectionException(ex);
}
}
else {
txObject.getUserTransaction().begin();
}
}
/**
@@ -1007,7 +1053,14 @@ public class JtaTransactionManager extends AbstractPlatformTransactionManager
}
throw new UnexpectedRollbackException("JTA transaction already rolled back (probably due to a timeout)");
}
txObject.getUserTransaction().commit();
if (this.enforceReadOnly && status.isReadOnly()) {
// JTA 2.1 isReadOnly enforces a rollback call
txObject.getUserTransaction().rollback();
}
else {
txObject.getUserTransaction().commit();
}
}
catch (RollbackException ex) {
throw new UnexpectedRollbackException(
@@ -16,15 +16,22 @@
package org.springframework.transaction.jta;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import jakarta.transaction.HeuristicMixedException;
import jakarta.transaction.HeuristicRollbackException;
import jakarta.transaction.NotSupportedException;
import jakarta.transaction.RollbackException;
import jakarta.transaction.SystemException;
import jakarta.transaction.Transaction;
import jakarta.transaction.TransactionManager;
import jakarta.transaction.UserTransaction;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* Adapter for a JTA UserTransaction handle, taking a JTA
@@ -40,11 +47,22 @@ import org.springframework.util.Assert;
* <p>Used internally by Spring's {@link JtaTransactionManager} for certain
* scenarios. Not intended for direct use in application code.
*
* <p>As of Spring Framework 7.1, this adapter supports the JTA 2.1
* read-only methods as well.
*
* @author Juergen Hoeller
* @since 1.1.5
*/
public class UserTransactionAdapter implements UserTransaction {
// JTA 2.1 TransactionManager#begin(boolean) method available?
private static final @Nullable Method beginWithReadOnlyMethod =
ClassUtils.getMethodIfAvailable(TransactionManager.class, "begin", boolean.class);
// JTA 2.1 Transaction#begin(boolean) method available?
private static final @Nullable Method isReadOnlyMethod =
ClassUtils.getMethodIfAvailable(Transaction.class, "isReadOnly");
private final TransactionManager transactionManager;
@@ -75,10 +93,41 @@ public class UserTransactionAdapter implements UserTransaction {
this.transactionManager.begin();
}
/**
* JTA 2.1 begin(boolean) method.
* @since 7.1
*/
// @Override - on JTA 2.1
public void begin(boolean isReadOnly) throws NotSupportedException, SystemException {
if (beginWithReadOnlyMethod == null) {
if (isReadOnly) {
throw new NotSupportedException("begin(true) requires JTA 2.1");
}
this.transactionManager.begin();
return;
}
try {
beginWithReadOnlyMethod.invoke(this.transactionManager, isReadOnly);
}
catch (Exception ex) {
if (ex instanceof InvocationTargetException ite) {
if (ite.getTargetException() instanceof NotSupportedException nse) {
throw nse;
}
if (ite.getTargetException() instanceof SystemException se) {
throw se;
}
}
ReflectionUtils.handleReflectionException(ex);
}
}
@Override
public void commit()
throws RollbackException, HeuristicMixedException, HeuristicRollbackException,
SecurityException, SystemException {
this.transactionManager.commit();
}
@@ -97,4 +146,27 @@ public class UserTransactionAdapter implements UserTransaction {
return this.transactionManager.getStatus();
}
/**
* JTA 2.1 isReadOnly() method.
* @since 7.1
*/
// @Override - on JTA 2.1
public boolean isReadOnly() throws SystemException {
if (isReadOnlyMethod != null) {
Transaction transaction = this.transactionManager.getTransaction();
try {
return (Boolean) isReadOnlyMethod.invoke(transaction);
}
catch (Exception ex) {
if (ex instanceof InvocationTargetException ite &&
ite.getTargetException() instanceof SystemException se) {
throw se;
}
ReflectionUtils.handleReflectionException(ex);
}
}
return false;
}
}
@@ -558,6 +558,8 @@ class JtaTransactionManagerTests {
given(tm.suspend()).willReturn(tx);
final JtaTransactionManager ptm = newJtaTransactionManager(ut, tm);
// ptm.setEnforceReadOnly(true);
TransactionTemplate tt = new TransactionTemplate(ptm);
tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
tt.setName("txName");
@@ -584,8 +586,16 @@ class JtaTransactionManagerTests {
});
assertThat(TransactionSynchronizationManager.isSynchronizationActive()).isFalse();
// Traditional JTA transaction
verify(ut, times(2)).begin();
verify(ut, times(2)).commit();
// JTA 2.1 with setEnforceReadOnly(true)
// verify(ut, times(1)).begin();
// verify(ut, times(1)).begin(true);
// verify(ut, times(1)).rollback();
// verify(ut, times(1)).commit();
verify(tm).resume(tx);
}