Support pattern matching for method names in ControlFlowPointcut
Prior to this commit, ControlFlowPointcut supported a single method name which was matched exactly. Although it was possible to extend ControlFlowPointcut to add support for pattern matching, it was a bit cumbersome. To address that, this commit introduces built-in pattern matching support for method names in ControlFlowPointcut, analogous to the pattern matching support in NameMatchMethodPointcut. Specifically, a user can provide one or more method name patterns, and the patterns will be matched against candidate method names using OR semantics. By default, the matching algorithm delegates to PatternMatchUtils.simpleMatch(), but this can be overridden in subclasses by overriding the new protected isMatch() method. Closes gh-31435
This commit is contained in:
@@ -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<String> trackedMethodNamePatterns() {
|
||||
return super.methodNamePatterns;
|
||||
}
|
||||
}
|
||||
|
||||
CustomControlFlowPointcut cflow = new CustomControlFlowPointcut(MyComponent.class, "getAge");
|
||||
CustomControlFlowPointcut cflow = new CustomControlFlowPointcut(MyComponent.class, "set*", "getAge", "set*", "set*");
|
||||
|
||||
assertThat(cflow.trackedClass()).isEqualTo(MyComponent.class);
|
||||
assertThat(cflow.trackedMethod()).isEqualTo("getAge");
|
||||
assertThat(cflow.trackedMethodNamePatterns()).containsExactly("set*", "getAge");
|
||||
|
||||
MyComponent component = new MyComponent();
|
||||
TestBean target = new TestBean("Jane", 27);
|
||||
NopInterceptor nop = new NopInterceptor();
|
||||
ProxyFactory pf = new ProxyFactory(target);
|
||||
pf.addAdvisor(new DefaultPointcutAdvisor(cflow, nop));
|
||||
ITestBean proxy = (ITestBean) pf.getProxy();
|
||||
|
||||
// Not advised: the proxy is not invoked under MyComponent#getAge
|
||||
// Will not be advised: the proxy is not invoked under MyComponent#getAge
|
||||
assertThat(proxy.getAge()).isEqualTo(target.getAge());
|
||||
assertThat(nop.getCount()).isEqualTo(0);
|
||||
assertThat(cflow.getEvaluations()).isEqualTo(2); // intentional double increment
|
||||
|
||||
// Will be advised: the proxy is invoked under MyComponent#getAge
|
||||
assertThat(new MyComponent().getAge(proxy)).isEqualTo(target.getAge());
|
||||
assertThat(component.getAge(proxy)).isEqualTo(target.getAge());
|
||||
assertThat(nop.getCount()).isEqualTo(1);
|
||||
assertThat(cflow.getEvaluations()).isEqualTo(4); // intentional double increment
|
||||
|
||||
// Won't be advised: the proxy is not invoked under MyComponent#getAge
|
||||
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(6); // intentional double increment
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that we can use a cflow pointcut only in conjunction with
|
||||
* Check that we can use a cflow pointcut in conjunction with
|
||||
* a static pointcut: e.g. all setter methods that are invoked under
|
||||
* a particular class. This greatly reduces the number of calls
|
||||
* to the cflow pointcut, meaning that it's not so prohibitively
|
||||
@@ -122,24 +157,28 @@ class ControlFlowPointcutTests {
|
||||
*/
|
||||
@Test
|
||||
void selectiveApplication() {
|
||||
MyComponent component = new MyComponent();
|
||||
TestBean target = new TestBean("Jane", 27);
|
||||
ControlFlowPointcut cflow = new ControlFlowPointcut(MyComponent.class);
|
||||
ControlFlowPointcut cflow = pointcut();
|
||||
NopInterceptor nop = new NopInterceptor();
|
||||
Pointcut settersUnderMyComponent = Pointcuts.intersection(Pointcuts.SETTERS, cflow);
|
||||
ProxyFactory pf = new ProxyFactory(target);
|
||||
pf.addAdvisor(new DefaultPointcutAdvisor(settersUnderMyComponent, nop));
|
||||
ITestBean proxy = (ITestBean) pf.getProxy();
|
||||
|
||||
// Not advised, not under MyComponent
|
||||
// Will not be advised: not under MyComponent
|
||||
target.setAge(16);
|
||||
assertThat(nop.getCount()).isEqualTo(0);
|
||||
assertThat(cflow.getEvaluations()).isEqualTo(0);
|
||||
|
||||
// Not advised; under MyComponent but not a setter
|
||||
assertThat(new MyComponent().getAge(proxy)).isEqualTo(16);
|
||||
// Will not be advised: under MyComponent but not a setter
|
||||
assertThat(component.getAge(proxy)).isEqualTo(16);
|
||||
assertThat(nop.getCount()).isEqualTo(0);
|
||||
assertThat(cflow.getEvaluations()).isEqualTo(0);
|
||||
|
||||
// Won't be advised
|
||||
new MyComponent().set(proxy);
|
||||
// Will be advised due to Pointcuts.SETTERS: the proxy is invoked under MyComponent#set
|
||||
component.set(proxy);
|
||||
assertThat(proxy.getAge()).isEqualTo(5);
|
||||
assertThat(nop.getCount()).isEqualTo(1);
|
||||
|
||||
// We saved most evaluations
|
||||
@@ -148,21 +187,63 @@ class ControlFlowPointcutTests {
|
||||
|
||||
@Test
|
||||
void equalsAndHashCode() {
|
||||
assertThat(new ControlFlowPointcut(MyComponent.class)).isEqualTo(new ControlFlowPointcut(MyComponent.class));
|
||||
assertThat(new ControlFlowPointcut(MyComponent.class, "getAge")).isEqualTo(new ControlFlowPointcut(MyComponent.class, "getAge"));
|
||||
assertThat(new ControlFlowPointcut(MyComponent.class, "getAge")).isNotEqualTo(new ControlFlowPointcut(MyComponent.class));
|
||||
assertThat(pointcut()).isEqualTo(pointcut());
|
||||
assertThat(pointcut()).hasSameHashCodeAs(pointcut());
|
||||
|
||||
assertThat(new ControlFlowPointcut(MyComponent.class)).hasSameHashCodeAs(new ControlFlowPointcut(MyComponent.class));
|
||||
assertThat(new ControlFlowPointcut(MyComponent.class, "getAge")).hasSameHashCodeAs(new ControlFlowPointcut(MyComponent.class, "getAge"));
|
||||
assertThat(new ControlFlowPointcut(MyComponent.class, "getAge")).doesNotHaveSameHashCodeAs(new ControlFlowPointcut(MyComponent.class));
|
||||
assertThat(pointcut("getAge")).isEqualTo(pointcut("getAge"));
|
||||
assertThat(pointcut("getAge")).hasSameHashCodeAs(pointcut("getAge"));
|
||||
|
||||
assertThat(pointcut("getAge")).isNotEqualTo(pointcut());
|
||||
assertThat(pointcut("getAge")).doesNotHaveSameHashCodeAs(pointcut());
|
||||
|
||||
assertThat(pointcut("get*", "set*")).isEqualTo(pointcut("get*", "set*"));
|
||||
assertThat(pointcut("get*", "set*")).isEqualTo(pointcut("get*", "set*", "set*", "get*"));
|
||||
assertThat(pointcut("get*", "set*")).hasSameHashCodeAs(pointcut("get*", "get*", "set*"));
|
||||
|
||||
assertThat(pointcut("get*", "set*")).isNotEqualTo(pointcut("set*", "get*"));
|
||||
assertThat(pointcut("get*", "set*")).doesNotHaveSameHashCodeAs(pointcut("set*", "get*"));
|
||||
|
||||
assertThat(pointcut("get*", "set*")).isEqualTo(pointcut(List.of("get*", "set*")));
|
||||
assertThat(pointcut("get*", "set*")).isEqualTo(pointcut(List.of("get*", "set*", "set*", "get*")));
|
||||
assertThat(pointcut("get*", "set*")).hasSameHashCodeAs(pointcut(List.of("get*", "get*", "set*")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testToString() {
|
||||
assertThat(new ControlFlowPointcut(MyComponent.class)).asString()
|
||||
.isEqualTo(ControlFlowPointcut.class.getName() + ": class = " + MyComponent.class.getName() + "; methodName = null");
|
||||
assertThat(new ControlFlowPointcut(MyComponent.class, "getAge")).asString()
|
||||
.isEqualTo(ControlFlowPointcut.class.getName() + ": class = " + MyComponent.class.getName() + "; methodName = getAge");
|
||||
String pointcutType = ControlFlowPointcut.class.getName();
|
||||
String componentType = MyComponent.class.getName();
|
||||
|
||||
assertThat(pointcut()).asString()
|
||||
.startsWith(pointcutType)
|
||||
.contains(componentType)
|
||||
.endsWith("[]");
|
||||
|
||||
assertThat(pointcut("getAge")).asString()
|
||||
.startsWith(pointcutType)
|
||||
.contains(componentType)
|
||||
.endsWith("[getAge]");
|
||||
|
||||
assertThat(pointcut("get*", "set*", "get*")).asString()
|
||||
.startsWith(pointcutType)
|
||||
.contains(componentType)
|
||||
.endsWith("[get*, set*]");
|
||||
}
|
||||
|
||||
|
||||
private static ControlFlowPointcut pointcut() {
|
||||
return new ControlFlowPointcut(MyComponent.class);
|
||||
}
|
||||
|
||||
private static ControlFlowPointcut pointcut(String methodNamePattern) {
|
||||
return new ControlFlowPointcut(MyComponent.class, methodNamePattern);
|
||||
}
|
||||
|
||||
private static ControlFlowPointcut pointcut(String... methodNamePatterns) {
|
||||
return new ControlFlowPointcut(MyComponent.class, methodNamePatterns);
|
||||
}
|
||||
|
||||
private static ControlFlowPointcut pointcut(List<String> methodNamePatterns) {
|
||||
return new ControlFlowPointcut(MyComponent.class, methodNamePatterns);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user