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
This commit is contained in:
Sam Brannen 2025-07-05 12:41:24 +02:00
parent 33f51b183d
commit 17df4b4c38
8 changed files with 57 additions and 45 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -65,7 +65,7 @@ public record MethodRetrySpec(
MethodRetryPredicate combinedPredicate() {
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);
}

View File

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

View File

@ -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
* {@code candidate}.

View File

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