diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java index d1a52caf50..6eb7086c07 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java @@ -50,38 +50,42 @@ import org.springframework.transaction.TransactionDefinition; * exceptions. * *

Rollback rules determine if a transaction should be rolled back when a given - * exception is thrown, and the rules are based on patterns. A pattern can be a - * fully qualified class name or a substring of a fully qualified class name for - * an exception type (which must be a subclass of {@code Throwable}), with no + * exception is thrown, and the rules are based on types or patterns. Custom + * rules may be configured via {@link #rollbackFor}/{@link #noRollbackFor} and + * {@link #rollbackForClassName}/{@link #noRollbackForClassName}, which allow + * rules to be specified as types or patterns, respectively. + * + *

When a rollback rule is defined with an exception type, that type will be + * used to match against the type of a thrown exception and its super types, + * providing type safety and avoiding any unintentional matches that may occur + * when using a pattern. For example, a value of + * {@code jakarta.servlet.ServletException.class} will only match thrown exceptions + * of type {@code jakarta.servlet.ServletException} and its subclasses. + * + *

When a rollback rule is defined with an exception pattern, the pattern can + * be a fully qualified class name or a substring of a fully qualified class name + * for an exception type (which must be a subclass of {@code Throwable}), with no * wildcard support at present. For example, a value of * {@code "jakarta.servlet.ServletException"} or {@code "ServletException"} will * match {@code jakarta.servlet.ServletException} and its subclasses. * - *

Rollback rules may be configured via {@link #rollbackFor}/{@link #noRollbackFor} - * and {@link #rollbackForClassName}/{@link #noRollbackForClassName}, which allow - * patterns to be specified as {@link Class} references or {@linkplain String - * strings}, respectively. When an exception type is specified as a class reference - * its fully qualified name will be used as the pattern. Consequently, - * {@code @Transactional(rollbackFor = example.CustomException.class)} is equivalent - * to {@code @Transactional(rollbackForClassName = "example.CustomException")}. - * - *

WARNING: You must carefully consider how specific the pattern + *

WARNING: You must carefully consider how specific a pattern * is and whether to include package information (which isn't mandatory). For example, * {@code "Exception"} will match nearly anything and will probably hide other * rules. {@code "java.lang.Exception"} would be correct if {@code "Exception"} * were meant to define a rule for all checked exceptions. With more unique * exception names such as {@code "BaseBusinessException"} there is likely no * need to use the fully qualified class name for the exception pattern. Furthermore, - * rollback rules may result in unintentional matches for similarly named exceptions - * and nested classes. This is due to the fact that a thrown exception is considered - * to be a match for a given rollback rule if the name of thrown exception contains - * the exception pattern configured for the rollback rule. For example, given a - * rule configured to match on {@code com.example.CustomException}, that rule - * would match against an exception named - * {@code com.example.CustomExceptionV2} (an exception in the same package as + * rollback rules defined via patterns may result in unintentional matches for + * similarly named exceptions and nested classes. This is due to the fact that a + * thrown exception is considered to be a match for a given pattern-based rollback + * rule if the name of thrown exception contains the exception pattern configured + * for the rollback rule. For example, given a rule configured to match against + * {@code "com.example.CustomException"}, that rule will match against an exception + * named {@code com.example.CustomExceptionV2} (an exception in the same package as * {@code CustomException} but with an additional suffix) or an exception named - * {@code com.example.CustomException$AnotherException} - * (an exception declared as a nested class in {@code CustomException}). + * {@code com.example.CustomException$AnotherException} (an exception declared as + * a nested class in {@code CustomException}). * *

For specific information about the semantics of other attributes in this * annotation, consult the {@link org.springframework.transaction.TransactionDefinition} @@ -207,7 +211,7 @@ public @interface Transactional { boolean readOnly() default false; /** - * Defines zero (0) or more exception {@linkplain Class classes}, which must be + * Defines zero (0) or more exception {@linkplain Class types}, which must be * subclasses of {@link Throwable}, indicating which exception types must cause * a transaction rollback. *

By default, a transaction will be rolled back on {@link RuntimeException} @@ -215,10 +219,9 @@ public @interface Transactional { * {@link org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)} * for a detailed explanation. *

This is the preferred way to construct a rollback rule (in contrast to - * {@link #rollbackForClassName}), matching the exception type, its subclasses, - * and its nested classes. See the {@linkplain Transactional class-level javadocs} - * for further details on rollback rule semantics and warnings regarding possible - * unintentional matches. + * {@link #rollbackForClassName}), matching the exception type and its subclasses + * in a type-safe manner. See the {@linkplain Transactional class-level javadocs} + * for further details on rollback rule semantics. * @see #rollbackForClassName * @see org.springframework.transaction.interceptor.RollbackRuleAttribute#RollbackRuleAttribute(Class) * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable) @@ -239,14 +242,13 @@ public @interface Transactional { String[] rollbackForClassName() default {}; /** - * Defines zero (0) or more exception {@link Class Classes}, which must be + * Defines zero (0) or more exception {@link Class types}, which must be * subclasses of {@link Throwable}, indicating which exception types must * not cause a transaction rollback. *

This is the preferred way to construct a rollback rule (in contrast to - * {@link #noRollbackForClassName}), matching the exception type, its subclasses, - * and its nested classes. See the {@linkplain Transactional class-level javadocs} - * for further details on rollback rule semantics and warnings regarding possible - * unintentional matches. + * {@link #noRollbackForClassName}), matching the exception type and its subclasses + * in a type-safe manner. See the {@linkplain Transactional class-level javadocs} + * for further details on rollback rule semantics. * @see #noRollbackForClassName * @see org.springframework.transaction.interceptor.NoRollbackRuleAttribute#NoRollbackRuleAttribute(Class) * @see org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable) diff --git a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java index fff46ec2a1..1b55b0ad4c 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java +++ b/spring-tx/src/main/java/org/springframework/transaction/interceptor/RollbackRuleAttribute.java @@ -27,21 +27,28 @@ import org.springframework.util.Assert; *

Multiple such rules can be applied to determine whether a transaction * should commit or rollback after an exception has been thrown. * - *

Each rule is based on an exception pattern which can be a fully qualified - * class name or a substring of a fully qualified class name for an exception - * type (which must be a subclass of {@code Throwable}), with no wildcard support - * at present. For example, a value of {@code "jakarta.servlet.ServletException"} - * or {@code "ServletException"} would match {@code jakarta.servlet.ServletException} - * and its subclasses. + *

Each rule is based on an exception type or exception pattern, supplied via + * {@link #RollbackRuleAttribute(Class)} or {@link #RollbackRuleAttribute(String)}, + * respectively. * - *

An exception pattern can be specified as a {@link Class} reference or a - * {@link String} in {@link #RollbackRuleAttribute(Class)} and - * {@link #RollbackRuleAttribute(String)}, respectively. When an exception type - * is specified as a class reference its fully qualified name will be used as the - * pattern. See the javadocs for + *

When a rollback rule is defined with an exception type, that type will be + * used to match against the type of a thrown exception and its super types, + * providing type safety and avoiding any unintentional matches that may occur + * when using a pattern. For example, a value of + * {@code jakarta.servlet.ServletException.class} will only match thrown exceptions + * of type {@code jakarta.servlet.ServletException} and its subclasses. + * + *

When a rollback rule is defined with an exception pattern, the pattern can + * be a fully qualified class name or a substring of a fully qualified class name + * for an exception type (which must be a subclass of {@code Throwable}), with no + * wildcard support at present. For example, a value of + * {@code "jakarta.servlet.ServletException"} or {@code "ServletException"} will + * match {@code jakarta.servlet.ServletException} and its subclasses. + * + *

See the javadocs for * {@link org.springframework.transaction.annotation.Transactional @Transactional} * for further details on rollback rule semantics, patterns, and warnings regarding - * possible unintentional matches. + * possible unintentional matches with pattern-based rules. * * @author Rod Johnson * @author Sam Brannen @@ -60,27 +67,36 @@ public class RollbackRuleAttribute implements Serializable{ /** - * Could hold exception, resolving class name but would always require FQN. - * This way does multiple string comparisons, but how often do we decide - * whether to roll back a transaction following an exception? + * Exception pattern: used when searching for matches in a thrown exception's + * class hierarchy based on names of exceptions, with zero type safety and + * potentially resulting in unintentional matches for similarly named exception + * types and nested exception types. */ private final String exceptionPattern; + /** + * Exception type: used to ensure type safety when searching for matches in + * a thrown exception's class hierarchy. + * @since 6.0 + */ + @Nullable + private final Class exceptionType; + /** * Create a new instance of the {@code RollbackRuleAttribute} class * for the given {@code exceptionType}. *

This is the preferred way to construct a rollback rule that matches - * the supplied exception type, its subclasses, and its nested classes. + * the supplied exception type and its subclasses with type safety. *

See the javadocs for * {@link org.springframework.transaction.annotation.Transactional @Transactional} - * for further details on rollback rule semantics, patterns, and warnings regarding - * possible unintentional matches. + * for further details on rollback rule semantics. * @param exceptionType exception type; must be {@link Throwable} or a subclass * of {@code Throwable} * @throws IllegalArgumentException if the supplied {@code exceptionType} is * not a {@code Throwable} type or is {@code null} */ + @SuppressWarnings("unchecked") public RollbackRuleAttribute(Class exceptionType) { Assert.notNull(exceptionType, "'exceptionType' cannot be null"); if (!Throwable.class.isAssignableFrom(exceptionType)) { @@ -88,6 +104,7 @@ public class RollbackRuleAttribute implements Serializable{ "Cannot construct rollback rule from [" + exceptionType.getName() + "]: it's not a Throwable"); } this.exceptionPattern = exceptionType.getName(); + this.exceptionType = (Class) exceptionType; } /** @@ -97,6 +114,8 @@ public class RollbackRuleAttribute implements Serializable{ * {@link org.springframework.transaction.annotation.Transactional @Transactional} * for further details on rollback rule semantics, patterns, and warnings regarding * possible unintentional matches. + *

For improved type safety and to avoid unintentional matches, use + * {@link #RollbackRuleAttribute(Class)} instead. * @param exceptionPattern the exception name pattern; can also be a fully * package-qualified class name * @throws IllegalArgumentException if the supplied {@code exceptionPattern} @@ -105,6 +124,7 @@ public class RollbackRuleAttribute implements Serializable{ public RollbackRuleAttribute(String exceptionPattern) { Assert.hasText(exceptionPattern, "'exceptionPattern' cannot be null or empty"); this.exceptionPattern = exceptionPattern; + this.exceptionType = null; } @@ -129,7 +149,8 @@ public class RollbackRuleAttribute implements Serializable{ *

When comparing roll back rules that match against a given exception, a rule * with a lower matching depth wins. For example, a direct match ({@code depth == 0}) * wins over a match in the superclass hierarchy ({@code depth > 0}). - *

A match against a nested exception type or similarly named exception type + *

When constructed with an exception pattern via {@link #RollbackRuleAttribute(String)}, + * a match against a nested exception type or similarly named exception type * will return a depth signifying a match at the corresponding level in the * class hierarchy as if there had been a direct match. */ @@ -139,7 +160,13 @@ public class RollbackRuleAttribute implements Serializable{ private int getDepth(Class exceptionType, int depth) { - if (exceptionType.getName().contains(this.exceptionPattern)) { + if (this.exceptionType != null) { + if (this.exceptionType.equals(exceptionType)) { + // Found it! + return depth; + } + } + else if (exceptionType.getName().contains(this.exceptionPattern)) { // Found it! return depth; } diff --git a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java index f210659af3..6b1cb675d0 100644 --- a/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java +++ b/spring-tx/src/test/java/org/springframework/transaction/interceptor/RollbackRuleAttributeTests.java @@ -51,15 +51,6 @@ class RollbackRuleAttributeTests { assertThat(rr.getDepth(new MyRuntimeException())).isEqualTo(-1); } - @Test - void alwaysFoundForThrowable() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(Throwable.class.getName()); - assertThat(rr.getDepth(new MyRuntimeException())).isGreaterThan(0); - assertThat(rr.getDepth(new IOException())).isGreaterThan(0); - assertThat(rr.getDepth(new FatalBeanException(null, null))).isGreaterThan(0); - assertThat(rr.getDepth(new RuntimeException())).isGreaterThan(0); - } - @Test void foundImmediatelyWhenDirectMatch() { RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class.getName()); @@ -74,6 +65,9 @@ class RollbackRuleAttributeTests { @Test void foundImmediatelyWhenNameOfExceptionThrownStartsWithNameOfRegisteredException() { + // Precondition for this use case. + assertThat(MyException.class.isAssignableFrom(MyException2.class)).isFalse(); + RollbackRuleAttribute rr = new RollbackRuleAttribute(MyException.class.getName()); assertThat(rr.getDepth(new MyException2())).isEqualTo(0); } @@ -85,6 +79,15 @@ class RollbackRuleAttributeTests { assertThat(rr.getDepth(new MyRuntimeException())).isEqualTo(3); } + @Test + void alwaysFoundForThrowable() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Throwable.class.getName()); + assertThat(rr.getDepth(new MyRuntimeException())).isGreaterThan(0); + assertThat(rr.getDepth(new IOException())).isGreaterThan(0); + assertThat(rr.getDepth(new FatalBeanException(null, null))).isGreaterThan(0); + assertThat(rr.getDepth(new RuntimeException())).isGreaterThan(0); + } + } @Nested @@ -103,12 +106,18 @@ class RollbackRuleAttributeTests { } @Test - void alwaysFoundForThrowable() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(Throwable.class); - assertThat(rr.getDepth(new MyRuntimeException())).isGreaterThan(0); - assertThat(rr.getDepth(new IOException())).isGreaterThan(0); - assertThat(rr.getDepth(new FatalBeanException(null, null))).isGreaterThan(0); - assertThat(rr.getDepth(new RuntimeException())).isGreaterThan(0); + void notFoundWhenNameOfExceptionThrownStartsWithNameOfRegisteredException() { + // Precondition for this use case. + assertThat(MyException.class.isAssignableFrom(MyException2.class)).isFalse(); + + RollbackRuleAttribute rr = new RollbackRuleAttribute(MyException.class); + assertThat(rr.getDepth(new MyException2())).isEqualTo(-1); + } + + @Test + void notFoundWhenExceptionThrownIsNestedTypeOfRegisteredException() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(EnclosingException.class); + assertThat(rr.getDepth(new EnclosingException.NestedException())).isEqualTo(-1); } @Test @@ -117,18 +126,6 @@ class RollbackRuleAttributeTests { assertThat(rr.getDepth(new Exception())).isEqualTo(0); } - @Test - void foundImmediatelyWhenExceptionThrownIsNestedTypeOfRegisteredException() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(EnclosingException.class); - assertThat(rr.getDepth(new EnclosingException.NestedException())).isEqualTo(0); - } - - @Test - void foundImmediatelyWhenNameOfExceptionThrownStartsWithNameOfRegisteredException() { - RollbackRuleAttribute rr = new RollbackRuleAttribute(MyException.class); - assertThat(rr.getDepth(new MyException2())).isEqualTo(0); - } - @Test void foundInSuperclassHierarchy() { RollbackRuleAttribute rr = new RollbackRuleAttribute(Exception.class); @@ -136,6 +133,15 @@ class RollbackRuleAttributeTests { assertThat(rr.getDepth(new MyRuntimeException())).isEqualTo(3); } + @Test + void alwaysFoundForThrowable() { + RollbackRuleAttribute rr = new RollbackRuleAttribute(Throwable.class); + assertThat(rr.getDepth(new MyRuntimeException())).isGreaterThan(0); + assertThat(rr.getDepth(new IOException())).isGreaterThan(0); + assertThat(rr.getDepth(new FatalBeanException(null, null))).isGreaterThan(0); + assertThat(rr.getDepth(new RuntimeException())).isGreaterThan(0); + } + } diff --git a/src/docs/asciidoc/data-access.adoc b/src/docs/asciidoc/data-access.adoc index ec14b418c3..dc767498a1 100644 --- a/src/docs/asciidoc/data-access.adoc +++ b/src/docs/asciidoc/data-access.adoc @@ -1042,40 +1042,45 @@ including checked exceptions by specifying _rollback rules_. [NOTE] ==== Rollback rules determine if a transaction should be rolled back when a given exception is -thrown, and the rules are based on patterns. A pattern can be a fully qualified class -name or a substring of a fully qualified class name for an exception type (which must be -a subclass of `Throwable`), with no wildcard support at present. For example, a value of -`"jakarta.servlet.ServletException"` or `"ServletException"` will match -`jakarta.servlet.ServletException` and its subclasses. +thrown, and the rules are based on exception types or exception patterns. Rollback rules may be configured in XML via the `rollback-for` and `no-rollback-for` -attributes, which allow patterns to be specified as strings. When using +attributes, which allow rules to be defined as patterns. When using <>, rollback rules may be configured via the `rollbackFor`/`noRollbackFor` and -`rollbackForClassName`/`noRollbackForClassName` attributes, which allow patterns to be -specified as `Class` references or strings, respectively. When an exception type is -specified as a class reference its fully qualified name will be used as the pattern. -Consequently, `@Transactional(rollbackFor = example.CustomException.class)` is equivalent -to `@Transactional(rollbackForClassName = "example.CustomException")`. +`rollbackForClassName`/`noRollbackForClassName` attributes, which allow rules to be +defined based on exception types or patterns, respectively. + +When a rollback rule is defined with an exception type, that type will be used to match +against the type of a thrown exception and its super types, providing type safety and +avoiding any unintentional matches that may occur when using a pattern. For example, a +value of `jakarta.servlet.ServletException.class` will only match thrown exceptions of +type `jakarta.servlet.ServletException` and its subclasses. + +When a rollback rule is defined with an exception pattern, the pattern can be a fully +qualified class name or a substring of a fully qualified class name for an exception type +(which must be a subclass of `Throwable`), with no wildcard support at present. For +example, a value of `"jakarta.servlet.ServletException"` or `"ServletException"` will +match `jakarta.servlet.ServletException` and its subclasses. [WARNING] ===== -You must carefully consider how specific the pattern is and whether to include package +You must carefully consider how specific a pattern is and whether to include package information (which isn't mandatory). For example, `"Exception"` will match nearly anything and will probably hide other rules. `"java.lang.Exception"` would be correct if `"Exception"` were meant to define a rule for all checked exceptions. With more unique exception names such as `"BaseBusinessException"` there is likely no need to use the fully qualified class name for the exception pattern. -Furthermore, rollback rules may result in unintentional matches for similarly named -exceptions and nested classes. This is due to the fact that a thrown exception is -considered to be a match for a given rollback rule if the name of thrown exception -contains the exception pattern configured for the rollback rule. For example, given a -rule configured to match on `com.example.CustomException`, that rule would match against -an exception named `com.example.CustomExceptionV2` (an exception in the same package as -`CustomException` but with an additional suffix) or an exception named -`com.example.CustomException$AnotherException` (an exception declared as a nested class -in `CustomException`). +Furthermore, pattern-based rollback rules may result in unintentional matches for +similarly named exceptions and nested classes. This is due to the fact that a thrown +exception is considered to be a match for a given pattern-based rollback rule if the name +of the thrown exception contains the exception pattern configured for the rollback rule. +For example, given a rule configured to match on `"com.example.CustomException"`, that +rule will match against an exception named `com.example.CustomExceptionV2` (an exception +in the same package as `CustomException` but with an additional suffix) or an exception +named `com.example.CustomException$AnotherException` (an exception declared as a nested +class in `CustomException`). ===== ==== @@ -1770,7 +1775,7 @@ properties of the `@Transactional` annotation: TIP: See <> for further details on rollback rule semantics, patterns, and warnings regarding possible unintentional -matches. +matches for pattern-based rollback rules. Currently, you cannot have explicit control over the name of a transaction, where 'name' means the transaction name that appears in a transaction monitor, if applicable