Browse Source

Support direct matching against exceptions in ExceptionTypeFilter

Prior to this commit, ExceptionTypeFilter only supported matching
against an exception type. However, most use cases involve matching
against an exception instance. Moreover, every use case within the core
Spring Framework uses ExceptionTypeFilter to match against concrete
exception instances.

This commit therefore introduces an overloaded match(Throwable) method
in ExceptionTypeFilter in order to provide support for the most common
use cases.

See gh-35109
Closes gh-35160
pull/34709/head
Sam Brannen 6 months ago
parent
commit
17df4b4c38
  1. 2
      spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java
  2. 2
      spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java
  3. 8
      spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java
  4. 2
      spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java
  5. 2
      spring-context/src/main/java/org/springframework/resilience/retry/MethodRetrySpec.java
  6. 2
      spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java
  7. 10
      spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java
  8. 74
      spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java

2
spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java vendored

@ -62,7 +62,7 @@ class CachePutInterceptor extends AbstractKeyCacheInterceptor<CachePutOperation,
} }
catch (CacheOperationInvoker.ThrowableWrapper ex) { catch (CacheOperationInvoker.ThrowableWrapper ex) {
Throwable original = ex.getOriginal(); Throwable original = ex.getOriginal();
if (!earlyPut && operation.getExceptionTypeFilter().match(original.getClass())) { if (!earlyPut && operation.getExceptionTypeFilter().match(original)) {
cacheValue(context, value); cacheValue(context, value);
} }
throw ex; throw ex;

2
spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java vendored

@ -58,7 +58,7 @@ class CacheRemoveAllInterceptor extends AbstractCacheInterceptor<CacheRemoveAllO
} }
catch (CacheOperationInvoker.ThrowableWrapper ex) { catch (CacheOperationInvoker.ThrowableWrapper ex) {
Throwable original = ex.getOriginal(); Throwable original = ex.getOriginal();
if (!earlyRemove && operation.getExceptionTypeFilter().match(original.getClass())) { if (!earlyRemove && operation.getExceptionTypeFilter().match(original)) {
removeAll(context); removeAll(context);
} }
throw ex; throw ex;

8
spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java vendored

@ -56,12 +56,12 @@ class CacheRemoveEntryInterceptor extends AbstractKeyCacheInterceptor<CacheRemov
} }
return result; return result;
} }
catch (CacheOperationInvoker.ThrowableWrapper wrapperException) { catch (CacheOperationInvoker.ThrowableWrapper ex) {
Throwable ex = wrapperException.getOriginal(); Throwable original = ex.getOriginal();
if (!earlyRemove && operation.getExceptionTypeFilter().match(ex.getClass())) { if (!earlyRemove && operation.getExceptionTypeFilter().match(original)) {
removeValue(context); removeValue(context);
} }
throw wrapperException; throw ex;
} }
} }

2
spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java vendored

@ -92,7 +92,7 @@ class CacheResultInterceptor extends AbstractKeyCacheInterceptor<CacheResultOper
if (exceptionCache == null) { if (exceptionCache == null) {
return; return;
} }
if (filter.match(ex.getClass())) { if (filter.match(ex)) {
doPut(exceptionCache, cacheKey, ex); doPut(exceptionCache, cacheKey, ex);
} }
} }

2
spring-context/src/main/java/org/springframework/resilience/retry/MethodRetrySpec.java

@ -65,7 +65,7 @@ public record MethodRetrySpec(
MethodRetryPredicate combinedPredicate() { MethodRetryPredicate combinedPredicate() {
ExceptionTypeFilter exceptionFilter = new ExceptionTypeFilter(this.includes, this.excludes); ExceptionTypeFilter exceptionFilter = new ExceptionTypeFilter(this.includes, this.excludes);
return (method, throwable) -> exceptionFilter.match(throwable.getClass()) && return (method, throwable) -> exceptionFilter.match(throwable) &&
this.predicate.shouldRetry(method, throwable); this.predicate.shouldRetry(method, throwable);
} }

2
spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java

@ -59,7 +59,7 @@ class DefaultRetryPolicy implements RetryPolicy {
@Override @Override
public boolean shouldRetry(Throwable throwable) { public boolean shouldRetry(Throwable throwable) {
return this.exceptionFilter.match(throwable.getClass()) && return this.exceptionFilter.match(throwable) &&
(this.predicate == null || this.predicate.test(throwable)); (this.predicate == null || this.predicate.test(throwable));
} }

10
spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java

@ -64,6 +64,16 @@ public class ExceptionTypeFilter extends InstanceFilter<Class<? extends Throwabl
} }
/**
* Determine if the type of the supplied {@code exception} matches this filter.
* @since 7.0
* @see InstanceFilter#match(Object)
*/
public boolean match(Throwable exception) {
return match(exception.getClass());
}
/** /**
* Determine if the specified {@code instance} matches the specified * Determine if the specified {@code instance} matches the specified
* {@code candidate}. * {@code candidate}.

74
spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java

@ -33,81 +33,83 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
class ExceptionTypeFilterTests { class ExceptionTypeFilterTests {
ExceptionTypeFilter filter;
@Test @Test
void emptyFilter() { void emptyFilter() {
var filter = new ExceptionTypeFilter(null, null); filter = new ExceptionTypeFilter(null, null);
assertMatches(filter, Throwable.class); assertMatches(new Throwable());
assertMatches(filter, Error.class); assertMatches(new Error());
assertMatches(filter, Exception.class); assertMatches(new Exception());
assertMatches(filter, RuntimeException.class); assertMatches(new RuntimeException());
} }
@Test @Test
void includes() { void includes() {
var filter = new ExceptionTypeFilter(List.of(FileNotFoundException.class, IllegalArgumentException.class), null); filter = new ExceptionTypeFilter(List.of(FileNotFoundException.class, IllegalArgumentException.class), null);
assertMatches(filter, FileNotFoundException.class); assertMatches(new FileNotFoundException());
assertMatches(filter, IllegalArgumentException.class); assertMatches(new IllegalArgumentException());
assertMatches(filter, NumberFormatException.class); assertMatches(new NumberFormatException());
assertDoesNotMatch(filter, Throwable.class); assertDoesNotMatch(new Throwable());
assertDoesNotMatch(filter, FileSystemException.class); assertDoesNotMatch(new FileSystemException("test"));
} }
@Test @Test
void includesSubtypeMatching() { void includesSubtypeMatching() {
var filter = new ExceptionTypeFilter(List.of(RuntimeException.class), null); filter = new ExceptionTypeFilter(List.of(RuntimeException.class), null);
assertMatches(filter, RuntimeException.class); assertMatches(new RuntimeException());
assertMatches(filter, IllegalStateException.class); assertMatches(new IllegalStateException());
assertDoesNotMatch(filter, Exception.class); assertDoesNotMatch(new Exception());
} }
@Test @Test
void excludes() { void excludes() {
var filter = new ExceptionTypeFilter(null, List.of(FileNotFoundException.class, IllegalArgumentException.class)); filter = new ExceptionTypeFilter(null, List.of(FileNotFoundException.class, IllegalArgumentException.class));
assertDoesNotMatch(filter, FileNotFoundException.class); assertDoesNotMatch(new FileNotFoundException());
assertDoesNotMatch(filter, IllegalArgumentException.class); assertDoesNotMatch(new IllegalArgumentException());
assertMatches(filter, Throwable.class); assertMatches(new Throwable());
assertMatches(filter, AssertionError.class); assertMatches(new AssertionError());
assertMatches(filter, FileSystemException.class); assertMatches(new FileSystemException("test"));
} }
@Test @Test
void excludesSubtypeMatching() { void excludesSubtypeMatching() {
var filter = new ExceptionTypeFilter(null, List.of(IllegalArgumentException.class)); filter = new ExceptionTypeFilter(null, List.of(IllegalArgumentException.class));
assertDoesNotMatch(filter, IllegalArgumentException.class); assertDoesNotMatch(new IllegalArgumentException());
assertDoesNotMatch(filter, NumberFormatException.class); assertDoesNotMatch(new NumberFormatException());
assertMatches(filter, Throwable.class); assertMatches(new Throwable());
} }
@Test @Test
void includesAndExcludes() { void includesAndExcludes() {
var filter = new ExceptionTypeFilter(List.of(IOException.class), List.of(FileNotFoundException.class)); filter = new ExceptionTypeFilter(List.of(IOException.class), List.of(FileNotFoundException.class));
assertMatches(filter, IOException.class); assertMatches(new IOException());
assertMatches(filter, FileSystemException.class); assertMatches(new FileSystemException("test"));
assertDoesNotMatch(filter, FileNotFoundException.class); assertDoesNotMatch(new FileNotFoundException());
assertDoesNotMatch(filter, Throwable.class); assertDoesNotMatch(new Throwable());
} }
private static void assertMatches(ExceptionTypeFilter filter, Class<? extends Throwable> candidate) { private void assertMatches(Throwable candidate) {
assertThat(filter.match(candidate)) assertThat(this.filter.match(candidate))
.as("filter '" + filter + "' should match " + candidate.getSimpleName()) .as("filter '" + this.filter + "' should match " + candidate.getClass().getSimpleName())
.isTrue(); .isTrue();
} }
private static void assertDoesNotMatch(ExceptionTypeFilter filter, Class<? extends Throwable> candidate) { private void assertDoesNotMatch(Throwable candidate) {
assertThat(filter.match(candidate)) assertThat(this.filter.match(candidate))
.as("filter '" + filter + "' should not match " + candidate.getSimpleName()) .as("filter '" + this.filter + "' should not match " + candidate.getClass().getSimpleName())
.isFalse(); .isFalse();
} }

Loading…
Cancel
Save