diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java b/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java index 7b2b4cc32de..fa24bb10504 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java @@ -18,6 +18,7 @@ package org.springframework.aop.support; import java.io.Serializable; import java.util.Arrays; +import java.util.Objects; import org.springframework.aop.ClassFilter; import org.springframework.lang.Nullable; @@ -85,6 +86,18 @@ public abstract class ClassFilters { return new IntersectionClassFilter(classFilters); } + /** + * Return a class filter that represents the logical negation of the specified + * filter instance. + * @param classFilter the {@link ClassFilter} to negate + * @return a filter that represents the logical negation of the specified filter + * @since 6.1 + */ + public static ClassFilter negate(ClassFilter classFilter) { + Assert.notNull(classFilter, "ClassFilter must not be null"); + return new NegateClassFilter(classFilter); + } + /** * ClassFilter implementation for a union of the given ClassFilters. @@ -167,4 +180,40 @@ public abstract class ClassFilters { } + + /** + * ClassFilter implementation for a logical negation of the given ClassFilter. + */ + @SuppressWarnings("serial") + private static class NegateClassFilter implements ClassFilter, Serializable { + + private final ClassFilter original; + + NegateClassFilter(ClassFilter original) { + this.original = original; + } + + @Override + public boolean matches(Class clazz) { + return !this.original.matches(clazz); + } + + @Override + public boolean equals(Object other) { + return (this == other || (other instanceof NegateClassFilter that + && this.original.equals(that.original))); + } + + @Override + public int hashCode() { + return Objects.hash(getClass(), this.original); + } + + @Override + public String toString() { + return "Negate " + this.original; + } + + } + } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java b/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java index 22e8b44d2db..f2d226adfb2 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java @@ -18,6 +18,7 @@ package org.springframework.aop.support; import java.io.Serializable; import java.lang.reflect.Method; +import java.util.Objects; import org.springframework.aop.ClassFilter; import org.springframework.aop.IntroductionAwareMethodMatcher; @@ -81,6 +82,18 @@ public abstract class MethodMatchers { new IntersectionIntroductionAwareMethodMatcher(mm1, mm2) : new IntersectionMethodMatcher(mm1, mm2)); } + /** + * Return a method matcher that represents the logical negation of the specified + * matcher instance. + * @param methodMatcher the {@link MethodMatcher} to negate + * @return a matcher that represents the logical negation of the specified matcher + * @since 6.1 + */ + public static MethodMatcher negate(MethodMatcher methodMatcher) { + Assert.notNull(methodMatcher, "MethodMatcher must not be null"); + return new NegateMethodMatcher(methodMatcher); + } + /** * Apply the given MethodMatcher to the given Method, supporting an * {@link org.springframework.aop.IntroductionAwareMethodMatcher} @@ -338,4 +351,47 @@ public abstract class MethodMatchers { } } + + @SuppressWarnings("serial") + private static class NegateMethodMatcher implements MethodMatcher, Serializable { + + private final MethodMatcher original; + + NegateMethodMatcher(MethodMatcher original) { + this.original = original; + } + + @Override + public boolean matches(Method method, Class targetClass) { + return !this.original.matches(method, targetClass); + } + + @Override + public boolean isRuntime() { + return this.original.isRuntime(); + } + + @Override + public boolean matches(Method method, Class targetClass, Object... args) { + return !this.original.matches(method, targetClass, args); + } + + @Override + public boolean equals(Object other) { + return (this == other || (other instanceof NegateMethodMatcher that + && this.original.equals(that.original))); + } + + @Override + public int hashCode() { + return Objects.hash(getClass(), this.original); + } + + @Override + public String toString() { + return "Negate " + this.original; + } + + } + } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java index db83a9424df..b108eab36ac 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/ClassFiltersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,9 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.NestedRuntimeException; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Unit tests for {@link ClassFilters}. @@ -66,4 +69,65 @@ class ClassFiltersTests { .matches("^.+IntersectionClassFilter: \\[.+RootClassFilter: .+Exception, .+RootClassFilter: .+NestedRuntimeException\\]$"); } + @Test + void negateClassFilter() { + ClassFilter filter = mock(ClassFilter.class); + given(filter.matches(String.class)).willReturn(true); + ClassFilter negate = ClassFilters.negate(filter); + assertThat(negate.matches(String.class)).isFalse(); + verify(filter).matches(String.class); + } + + @Test + void negateTrueClassFilter() { + ClassFilter negate = ClassFilters.negate(ClassFilter.TRUE); + assertThat(negate.matches(String.class)).isFalse(); + assertThat(negate.matches(Object.class)).isFalse(); + assertThat(negate.matches(Integer.class)).isFalse(); + } + + @Test + void negateTrueClassFilterAppliedTwice() { + ClassFilter negate = ClassFilters.negate(ClassFilters.negate(ClassFilter.TRUE)); + assertThat(negate.matches(String.class)).isTrue(); + assertThat(negate.matches(Object.class)).isTrue(); + assertThat(negate.matches(Integer.class)).isTrue(); + } + + @Test + void negateIsNotEqualsToOriginalFilter() { + ClassFilter original = ClassFilter.TRUE; + ClassFilter negate = ClassFilters.negate(original); + assertThat(original).isNotEqualTo(negate); + } + + @Test + void negateOnSameFilterIsEquals() { + ClassFilter original = ClassFilter.TRUE; + ClassFilter first = ClassFilters.negate(original); + ClassFilter second = ClassFilters.negate(original); + assertThat(first).isEqualTo(second); + } + + @Test + void negateHasNotSameHashCodeAsOriginalFilter() { + ClassFilter original = ClassFilter.TRUE; + ClassFilter negate = ClassFilters.negate(original); + assertThat(original).doesNotHaveSameHashCodeAs(negate); + } + + @Test + void negateOnSameFilterHasSameHashCode() { + ClassFilter original = ClassFilter.TRUE; + ClassFilter first = ClassFilters.negate(original); + ClassFilter second = ClassFilters.negate(original); + assertThat(first).hasSameHashCodeAs(second); + } + + @Test + void toStringIncludesRepresentationOfOriginalFilter() { + ClassFilter original = ClassFilter.TRUE; + assertThat(ClassFilters.negate(original)).hasToString("Negate " + original); + } + } diff --git a/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java b/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java index 55a2d7cabb5..c33a8b548d0 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import org.springframework.core.testfixture.io.SerializationTestUtils; import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * @author Juergen Hoeller @@ -35,6 +36,8 @@ import static org.assertj.core.api.Assertions.assertThat; */ public class MethodMatchersTests { + private final static Method TEST_METHOD = mock(Method.class); + private final Method EXCEPTION_GETMESSAGE; private final Method ITESTBEAN_SETAGE; @@ -111,6 +114,65 @@ public class MethodMatchersTests { assertThat(second.equals(first)).isTrue(); } + @Test + void negateMethodMatcher() { + MethodMatcher getterMatcher = new StartsWithMatcher("get"); + MethodMatcher negate = MethodMatchers.negate(getterMatcher); + assertThat(negate.matches(ITESTBEAN_SETAGE, int.class)).isTrue(); + } + + @Test + void negateTrueMethodMatcher() { + MethodMatcher negate = MethodMatchers.negate(MethodMatcher.TRUE); + assertThat(negate.matches(TEST_METHOD, String.class)).isFalse(); + assertThat(negate.matches(TEST_METHOD, Object.class)).isFalse(); + assertThat(negate.matches(TEST_METHOD, Integer.class)).isFalse(); + } + + @Test + void negateTrueMethodMatcherAppliedTwice() { + MethodMatcher negate = MethodMatchers.negate(MethodMatchers.negate(MethodMatcher.TRUE)); + assertThat(negate.matches(TEST_METHOD, String.class)).isTrue(); + assertThat(negate.matches(TEST_METHOD, Object.class)).isTrue(); + assertThat(negate.matches(TEST_METHOD, Integer.class)).isTrue(); + } + + @Test + void negateIsNotEqualsToOriginalMatcher() { + MethodMatcher original = MethodMatcher.TRUE; + MethodMatcher negate = MethodMatchers.negate(original); + assertThat(original).isNotEqualTo(negate); + } + + @Test + void negateOnSameMatcherIsEquals() { + MethodMatcher original = MethodMatcher.TRUE; + MethodMatcher first = MethodMatchers.negate(original); + MethodMatcher second = MethodMatchers.negate(original); + assertThat(first).isEqualTo(second); + } + + @Test + void negateHasNotSameHashCodeAsOriginalMatcher() { + MethodMatcher original = MethodMatcher.TRUE; + MethodMatcher negate = MethodMatchers.negate(original); + assertThat(original).doesNotHaveSameHashCodeAs(negate); + } + + @Test + void negateOnSameMatcherHasSameHashCode() { + MethodMatcher original = MethodMatcher.TRUE; + MethodMatcher first = MethodMatchers.negate(original); + MethodMatcher second = MethodMatchers.negate(original); + assertThat(first).hasSameHashCodeAs(second); + } + + @Test + void toStringIncludesRepresentationOfOriginalMatcher() { + MethodMatcher original = MethodMatcher.TRUE; + assertThat(MethodMatchers.negate(original)).hasToString("Negate " + original); + } + public static class StartsWithMatcher extends StaticMethodMatcher {