Browse Source

Support matching against exception causes in ExceptionTypeFilter

Prior to this commit, ExceptionTypeFilter only provided support for
filtering based on exact matches against exception types; however, some
use cases require that filtering be applied to nested causes in a given
exception. For example, this functionality is a prerequisite for
gh-35583.

This commit introduces a new match(Throwable, boolean) method in
ExceptionTypeFilter, where the boolean flag enables matching against
nested exceptions.

See gh-35583
Closes gh-35592
pull/35603/head
Sam Brannen 6 months ago
parent
commit
670effa02b
  1. 50
      spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java
  2. 11
      spring-core/src/main/java/org/springframework/util/InstanceFilter.java
  3. 45
      spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java

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

@ -66,13 +66,59 @@ public class ExceptionTypeFilter extends InstanceFilter<Class<? extends Throwabl @@ -66,13 +66,59 @@ public class ExceptionTypeFilter extends InstanceFilter<Class<? extends Throwabl
/**
* Determine if the type of the supplied {@code exception} matches this filter.
* @param exception the exception to match against
* @return {@code true} if this filter matches the supplied exception
* @since 7.0
* @see InstanceFilter#match(Object)
* @see #match(Throwable, boolean)
*/
public boolean match(Throwable exception) {
return match(exception.getClass());
return match(exception, false);
}
/**
* Determine if the type of the supplied {@code exception} matches this filter,
* potentially matching against nested causes.
* @param exception the exception to match against
* @param traverseCauses whether the matching algorithm should recursively
* match against nested causes of the exception
* @return {@code true} if this filter matches the supplied exception or one
* of its nested causes
* @since 7.0
* @see InstanceFilter#match(Object)
*/
public boolean match(Throwable exception, boolean traverseCauses) {
return (traverseCauses ? matchTraversingCauses(exception) : match(exception.getClass()));
}
private boolean matchTraversingCauses(Throwable exception) {
Assert.notNull(exception, "Throwable to match must not be null");
boolean emptyIncludes = super.includes.isEmpty();
boolean emptyExcludes = super.excludes.isEmpty();
if (emptyIncludes && emptyExcludes) {
return super.matchIfEmpty;
}
if (!emptyExcludes && matchTraversingCauses(exception, super.excludes)) {
return false;
}
return (emptyIncludes || matchTraversingCauses(exception, super.includes));
}
private boolean matchTraversingCauses(
Throwable exception, Collection<? extends Class<? extends Throwable>> candidateTypes) {
for (Class<? extends Throwable> candidateType : candidateTypes) {
Throwable current = exception;
while (current != null) {
if (match(current.getClass(), candidateType)) {
return true;
}
current = current.getCause();
}
}
return false;
}
/**
* Determine if the specified {@code instance} matches the specified

11
spring-core/src/main/java/org/springframework/util/InstanceFilter.java

@ -18,6 +18,7 @@ package org.springframework.util; @@ -18,6 +18,7 @@ package org.springframework.util;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import org.jspecify.annotations.Nullable;
@ -35,11 +36,11 @@ import org.jspecify.annotations.Nullable; @@ -35,11 +36,11 @@ import org.jspecify.annotations.Nullable;
*/
public class InstanceFilter<T> {
private final Collection<? extends T> includes;
protected final Collection<? extends T> includes;
private final Collection<? extends T> excludes;
protected final Collection<? extends T> excludes;
private final boolean matchIfEmpty;
protected final boolean matchIfEmpty;
/**
@ -74,8 +75,8 @@ public class InstanceFilter<T> { @@ -74,8 +75,8 @@ public class InstanceFilter<T> {
public InstanceFilter(@Nullable Collection<? extends T> includes,
@Nullable Collection<? extends T> excludes, boolean matchIfEmpty) {
this.includes = (includes != null ? includes : Collections.emptyList());
this.excludes = (excludes != null ? excludes : Collections.emptyList());
this.includes = (includes != null ? Collections.unmodifiableCollection(includes) : Set.of());
this.excludes = (excludes != null ? Collections.unmodifiableCollection(excludes) : Set.of());
this.matchIfEmpty = matchIfEmpty;
}

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

@ -43,6 +43,11 @@ class ExceptionTypeFilterTests { @@ -43,6 +43,11 @@ class ExceptionTypeFilterTests {
assertMatches(new Error());
assertMatches(new Exception());
assertMatches(new RuntimeException());
assertMatchesCause(new Throwable());
assertMatchesCause(new Error());
assertMatchesCause(new Exception());
assertMatchesCause(new RuntimeException());
}
@Test
@ -67,6 +72,20 @@ class ExceptionTypeFilterTests { @@ -67,6 +72,20 @@ class ExceptionTypeFilterTests {
assertDoesNotMatch(new Exception());
}
@Test // gh-35583
void includesCauseAndSubtypeMatching() {
filter = new ExceptionTypeFilter(List.of(IOException.class), null);
assertMatchesCause(new IOException());
assertMatchesCause(new FileNotFoundException());
assertMatchesCause(new RuntimeException(new IOException()));
assertMatchesCause(new RuntimeException(new FileNotFoundException()));
assertMatchesCause(new Exception(new RuntimeException(new IOException())));
assertMatchesCause(new Exception(new RuntimeException(new FileNotFoundException())));
assertDoesNotMatchCause(new Exception());
}
@Test
void excludes() {
filter = new ExceptionTypeFilter(null, List.of(FileNotFoundException.class, IllegalArgumentException.class));
@ -89,6 +108,20 @@ class ExceptionTypeFilterTests { @@ -89,6 +108,20 @@ class ExceptionTypeFilterTests {
assertMatches(new Throwable());
}
@Test // gh-35583
void excludesCauseAndSubtypeMatching() {
filter = new ExceptionTypeFilter(null, List.of(IOException.class));
assertDoesNotMatchCause(new IOException());
assertDoesNotMatchCause(new FileNotFoundException());
assertDoesNotMatchCause(new RuntimeException(new IOException()));
assertDoesNotMatchCause(new RuntimeException(new FileNotFoundException()));
assertDoesNotMatchCause(new Exception(new RuntimeException(new IOException())));
assertDoesNotMatchCause(new Exception(new RuntimeException(new FileNotFoundException())));
assertMatchesCause(new Throwable());
}
@Test
void includesAndExcludes() {
filter = new ExceptionTypeFilter(List.of(IOException.class), List.of(FileNotFoundException.class));
@ -113,4 +146,16 @@ class ExceptionTypeFilterTests { @@ -113,4 +146,16 @@ class ExceptionTypeFilterTests {
.isFalse();
}
private void assertMatchesCause(Throwable candidate) {
assertThat(this.filter.match(candidate, true))
.as("filter '" + this.filter + "' should match " + candidate.getClass().getSimpleName())
.isTrue();
}
private void assertDoesNotMatchCause(Throwable candidate) {
assertThat(this.filter.match(candidate, true))
.as("filter '" + this.filter + "' should not match " + candidate.getClass().getSimpleName())
.isFalse();
}
}

Loading…
Cancel
Save