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 extends Throwable> 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 extends Throwable>) 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
<