diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/JtaTransactionAnnotationParser.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/JtaTransactionAnnotationParser.java index 26a7932a307..d54c01d1d31 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/JtaTransactionAnnotationParser.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/JtaTransactionAnnotationParser.java @@ -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; diff --git a/spring-tx/src/main/java/org/springframework/transaction/jta/JtaTransactionManager.java b/spring-tx/src/main/java/org/springframework/transaction/jta/JtaTransactionManager.java index 2c7c080859b..c45ac3519c7 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/jta/JtaTransactionManager.java +++ b/spring-tx/src/main/java/org/springframework/transaction/jta/JtaTransactionManager.java @@ -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. + *

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( diff --git a/spring-tx/src/main/java/org/springframework/transaction/jta/UserTransactionAdapter.java b/spring-tx/src/main/java/org/springframework/transaction/jta/UserTransactionAdapter.java index 28a996fc779..3c88cfd8957 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/jta/UserTransactionAdapter.java +++ b/spring-tx/src/main/java/org/springframework/transaction/jta/UserTransactionAdapter.java @@ -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; *

Used internally by Spring's {@link JtaTransactionManager} for certain * scenarios. Not intended for direct use in application code. * + *

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; + } + } diff --git a/spring-tx/src/test/java/org/springframework/transaction/JtaTransactionManagerTests.java b/spring-tx/src/test/java/org/springframework/transaction/JtaTransactionManagerTests.java index a7c8d99dba2..2680063e7d2 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/JtaTransactionManagerTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/JtaTransactionManagerTests.java @@ -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); }