diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java index cb2b368fd26..7f56a9c81f0 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java @@ -18,6 +18,9 @@ package org.springframework.aop.support; import java.io.Serializable; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.springframework.aop.ClassFilter; @@ -25,11 +28,15 @@ import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; +import org.springframework.util.PatternMatchUtils; /** * Pointcut and method matcher for use as a simple cflow-style pointcut. * + *
Each configured method name pattern can be an exact method name or a + * pattern (see {@link #isMatch(String, String)} for details on the supported + * pattern styles). + * *
Note that evaluating such pointcuts is 10-15 times slower than evaluating * normal pointcuts, but they are useful in some cases. * @@ -37,6 +44,9 @@ import org.springframework.util.ObjectUtils; * @author Rob Harrop * @author Juergen Hoeller * @author Sam Brannen + * @see #isMatch + * @see NameMatchMethodPointcut + * @see JdkRegexpMethodPointcut */ @SuppressWarnings("serial") public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher, Serializable { @@ -48,11 +58,10 @@ public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher protected final Class> clazz; /** - * The method against which to match, potentially {@code null}. - *
Available for use in subclasses since 6.1.
+ * An immutable list of method name patterns against which to match.
+ * @since 6.1
*/
- @Nullable
- protected final String methodName;
+ protected final List If no method name pattern is given, the pointcut matches all control flows
+ * below the given class.
+ * @param clazz the class
+ * @param methodNamePattern the method name pattern (may be {@code null})
+ */
+ public ControlFlowPointcut(Class> clazz, @Nullable String methodNamePattern) {
+ Assert.notNull(clazz, "Class must not be null");
+ this.clazz = clazz;
+ this.methodNamePatterns = (methodNamePattern != null ?
+ Collections.singletonList(methodNamePattern) : Collections.emptyList());
+ }
+
+ /**
+ * Construct a new pointcut that matches all calls below a method matching
+ * one of the given method name patterns in the given class.
+ * If no method name pattern is given, the pointcut matches all control flows
+ * below the given class.
+ * @param clazz the class
+ * @param methodNamePatterns the method name patterns (potentially empty)
+ * @since 6.1
+ */
+ public ControlFlowPointcut(Class> clazz, String... methodNamePatterns) {
+ this(clazz, Arrays.asList(methodNamePatterns));
}
/**
- * Construct a new pointcut that matches all calls below the given method
- * in the given class.
- * If no method name is given, the pointcut matches all control flows
+ * Construct a new pointcut that matches all calls below a method matching
+ * one of the given method name patterns in the given class.
+ * If no method name pattern is given, the pointcut matches all control flows
* below the given class.
* @param clazz the class
- * @param methodName the name of the method (may be {@code null})
+ * @param methodNamePatterns the method name patterns (potentially empty)
+ * @since 6.1
*/
- public ControlFlowPointcut(Class> clazz, @Nullable String methodName) {
+ public ControlFlowPointcut(Class> clazz, List The default implementation checks for direct equality as well as
+ * {@code xxx*}, {@code *xxx}, {@code *xxx*}, and {@code xxx*yyy} matches.
+ * Can be overridden in subclasses.
+ * @param methodName the method name to check
+ * @param methodNamePattern the method name pattern
+ * @return {@code true} if the method name matches the pattern
+ * @since 6.1
+ * @see #matches(Method, Class, Object...)
+ * @see PatternMatchUtils#simpleMatch(String, String)
+ */
+ protected boolean isMatch(String methodName, String methodNamePattern) {
+ return (methodName.equals(methodNamePattern) ||
+ PatternMatchUtils.simpleMatch(methodNamePattern, methodName));
+ }
+
@Override
public ClassFilter getClassFilter() {
@@ -149,22 +212,19 @@ public class ControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof ControlFlowPointcut that &&
- this.clazz.equals(that.clazz)) &&
- ObjectUtils.nullSafeEquals(this.methodName, that.methodName));
+ this.clazz.equals(that.clazz)) && this.methodNamePatterns.equals(that.methodNamePatterns));
}
@Override
public int hashCode() {
int code = this.clazz.hashCode();
- if (this.methodName != null) {
- code = 37 * code + this.methodName.hashCode();
- }
+ code = 37 * code + this.methodNamePatterns.hashCode();
return code;
}
@Override
public String toString() {
- return getClass().getName() + ": class = " + this.clazz.getName() + "; methodName = " + this.methodName;
+ return getClass().getName() + ": class = " + this.clazz.getName() + "; methodNamePatterns = " + this.methodNamePatterns;
}
}
diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java
index ab70cd7255a..d9cb49c1a21 100644
--- a/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java
+++ b/spring-aop/src/test/java/org/springframework/aop/support/ControlFlowPointcutTests.java
@@ -17,6 +17,7 @@
package org.springframework.aop.support;
import java.lang.reflect.Method;
+import java.util.List;
import org.junit.jupiter.api.Test;
@@ -39,36 +40,69 @@ class ControlFlowPointcutTests {
@Test
void matchesExactMethodName() {
+ MyComponent component = new MyComponent();
TestBean target = new TestBean("Jane", 27);
- ControlFlowPointcut cflow = new ControlFlowPointcut(MyComponent.class, "getAge");
+ ControlFlowPointcut cflow = pointcut("getAge");
NopInterceptor nop = new NopInterceptor();
ProxyFactory pf = new ProxyFactory(target);
pf.addAdvisor(new DefaultPointcutAdvisor(cflow, nop));
ITestBean proxy = (ITestBean) pf.getProxy();
- // Not advised, not under MyComponent
+ // Will not be advised: not under MyComponent
assertThat(proxy.getAge()).isEqualTo(target.getAge());
assertThat(nop.getCount()).isEqualTo(0);
assertThat(cflow.getEvaluations()).isEqualTo(1);
- // Will be advised
- assertThat(new MyComponent().getAge(proxy)).isEqualTo(target.getAge());
+ // Will be advised due to "getAge" pattern: the proxy is invoked under MyComponent#getAge
+ assertThat(component.getAge(proxy)).isEqualTo(target.getAge());
assertThat(nop.getCount()).isEqualTo(1);
assertThat(cflow.getEvaluations()).isEqualTo(2);
- // Won't be advised
- assertThat(new MyComponent().nomatch(proxy)).isEqualTo(target.getAge());
+ // Will not be advised: the proxy is invoked under MyComponent, but there is no match for "nomatch"
+ assertThat(component.nomatch(proxy)).isEqualTo(target.getAge());
assertThat(nop.getCount()).isEqualTo(1);
assertThat(cflow.getEvaluations()).isEqualTo(3);
}
+ @Test
+ void matchesMethodNamePatterns() {
+ MyComponent component = new MyComponent();
+ TestBean target = new TestBean("Jane", 27);
+ ControlFlowPointcut cflow = pointcut("foo", "get*", "bar", "*se*", "baz");
+ NopInterceptor nop = new NopInterceptor();
+ ProxyFactory pf = new ProxyFactory(target);
+ pf.addAdvisor(new DefaultPointcutAdvisor(cflow, nop));
+ ITestBean proxy = (ITestBean) pf.getProxy();
+
+ // Will not be advised: not under MyComponent
+ assertThat(proxy.getAge()).isEqualTo(target.getAge());
+ assertThat(nop.getCount()).isEqualTo(0);
+ assertThat(cflow.getEvaluations()).isEqualTo(1);
+
+ // Will be advised due to "get*" pattern: the proxy is invoked under MyComponent#getAge
+ assertThat(component.getAge(proxy)).isEqualTo(target.getAge());
+ assertThat(nop.getCount()).isEqualTo(1);
+ assertThat(cflow.getEvaluations()).isEqualTo(2);
+
+ // Will be advised due to "*se*" pattern: the proxy is invoked under MyComponent#set
+ component.set(proxy);
+ assertThat(proxy.getAge()).isEqualTo(5);
+ assertThat(nop.getCount()).isEqualTo(2);
+ assertThat(cflow.getEvaluations()).isEqualTo(4);
+
+ // Will not be advised: the proxy is invoked under MyComponent, but there is no match for "nomatch"
+ assertThat(component.nomatch(proxy)).isEqualTo(target.getAge());
+ assertThat(nop.getCount()).isEqualTo(2);
+ assertThat(cflow.getEvaluations()).isEqualTo(5);
+ }
+
@Test
void controlFlowPointcutIsExtensible() {
@SuppressWarnings("serial")
class CustomControlFlowPointcut extends ControlFlowPointcut {
- CustomControlFlowPointcut(Class> clazz, String methodName) {
- super(clazz, methodName);
+ CustomControlFlowPointcut(Class> clazz, String... methodNamePatterns) {
+ super(clazz, methodNamePatterns);
}
@Override
@@ -81,40 +115,41 @@ class ControlFlowPointcutTests {
return super.clazz;
}
- String trackedMethod() {
- return super.methodName;
+ List