From 670effa02bb6b18524057552939449e559d87172 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:46:41 +0200 Subject: [PATCH] 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 --- .../util/ExceptionTypeFilter.java | 50 ++++++++++++++++++- .../springframework/util/InstanceFilter.java | 11 ++-- .../util/ExceptionTypeFilterTests.java | 45 +++++++++++++++++ 3 files changed, 99 insertions(+), 7 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java b/spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java index 715084475aa..58d85b2d906 100644 --- a/spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java +++ b/spring-core/src/main/java/org/springframework/util/ExceptionTypeFilter.java @@ -66,13 +66,59 @@ public class ExceptionTypeFilter extends InstanceFilter> candidateTypes) { + + for (Class 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 diff --git a/spring-core/src/main/java/org/springframework/util/InstanceFilter.java b/spring-core/src/main/java/org/springframework/util/InstanceFilter.java index 75dfec80256..b670dc797c1 100644 --- a/spring-core/src/main/java/org/springframework/util/InstanceFilter.java +++ b/spring-core/src/main/java/org/springframework/util/InstanceFilter.java @@ -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; */ public class InstanceFilter { - private final Collection includes; + protected final Collection includes; - private final Collection excludes; + protected final Collection excludes; - private final boolean matchIfEmpty; + protected final boolean matchIfEmpty; /** @@ -74,8 +75,8 @@ public class InstanceFilter { public InstanceFilter(@Nullable Collection includes, @Nullable Collection 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; } diff --git a/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java b/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java index dd5bcea327e..1ef9594cecf 100644 --- a/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java +++ b/spring-core/src/test/java/org/springframework/util/ExceptionTypeFilterTests.java @@ -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 { 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 { 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 { .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(); + } + }