Support type-safe transaction rollback rules

Prior to this commit, there was no way to configure type-safe rollback
rules for transactions.

Even though a rollback rule could be defined using a Class reference
via the `rollbackFor` and `noRollbackFor` attributes in @Transactional,
those Class references got converted to Strings (as the fully qualified
class names of the exception types) in RollbackRuleAttribute which then
applied a pattern-based matching algorithm as if the Class references
had been supplied as Strings/patterns to begin with, thereby losing the
type information.

Pattern-based rollback rules suffer from the following three categories
of unintentional matches.

- identically named exceptions in different packages when the pattern
  does not include the package name -- for example,
  example.client.WebException and example.server.WebException both
  match against a "WebException" pattern.

- similarly named exceptions in the same package when a given exception
  name starts with the name of another exception -- for example,
  example.BusinessException and example.BusinessExceptionWithDetails
  both match against an "example.BusinessException" pattern.

- nested exceptions when an exception type is declared in another
  exception -- for example, example.BusinessException and
  example.BusinessException$NestedException both match against an
  "example.BusinessException" pattern.

This commit prevents the latter two categories of unintentional matches
for rollback rules defined using a Class reference by storing the
exceptionType in RollbackRuleAttribute and using that type in the
implementation of RollbackRuleAttribute.getDepth(Class, int), resulting
in type-safe rollback rules whenever the `rollbackFor` and
`noRollbackFor` attributes in `@Transactional` are used.

Note that the first category of unintentional matches never applied to
rollback rules created from a Class reference since the fully qualified
name of a Class reference always includes the package name.

Closes gh-28098
This commit is contained in:
Sam Brannen
2022-03-04 18:28:48 +01:00
parent 9cb4783296
commit c1033dbfb3
4 changed files with 140 additions and 100 deletions

View File

@@ -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);
}
}