Reading a book of "Pro Spring" came to an example
Pointcut pc = new ControlFlowPointcut(ControlFlowDemo.class, "test");
It's clear how it works, but question is - is it possible (and how) to point out a few methods in constructor? I mean what if I would like 1 pointcut that works for 3 methods (test1(2,3)). For instance like:
Pointcut pc = new ControlFlowPointcut(ControlFlowDemo.class, "test, test2, test3");
The answer to your question is no. ControlFlowPointcut
does not offer you to call it with multiple method names or to specify a pattern. The method name must match exactly, as you can see in the source code.
What you can do, however, is
cflow
and cflowbelow
pointcuts there, orprotected
in that class or at least have accessor methods for better extensibility, are actually private
, i.e. I would have ended up duplicating existing code. So I simply copied and extended it:package de.scrum_master.spring.q68431056;
import org.springframework.aop.ClassFilter;
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 java.io.Serializable;
import java.lang.reflect.Method;
import java.util.regex.Pattern;
/**
* Pointcut and method matcher for use in simple <b>cflow</b>-style pointcut.
* Note that evaluating such pointcuts is 10-15 times slower than evaluating
* normal pointcuts, but they are useful in some cases.
*
* @author Rod Johnson
* @author Rob Harrop
* @author Juergen Hoeller
* @author Alexander Kriegisch
*/
@SuppressWarnings("serial")
public class MultiMethodControlFlowPointcut implements Pointcut, ClassFilter, MethodMatcher, Serializable {
private Class<?> clazz;
@Nullable
private String methodName;
@Nullable
private Pattern methodPattern;
private volatile int evaluations;
/**
* Construct a new pointcut that matches all control flows below that class.
* @param clazz the clazz
*/
public MultiMethodControlFlowPointcut(Class<?> clazz) {
this(clazz, (String) null);
}
/**
* Construct a new pointcut that matches all calls below the given method
* in the given class. If no method name is given, matches all control flows
* below the given class.
* @param clazz the clazz
* @param methodName the name of the method (may be {@code null})
*/
public MultiMethodControlFlowPointcut(Class<?> clazz, @Nullable String methodName) {
Assert.notNull(clazz, "Class must not be null");
this.clazz = clazz;
this.methodName = methodName;
}
/**
* Construct a new pointcut that matches all calls below the given method
* in the given class. If no method name is given, matches all control flows
* below the given class.
* @param clazz the clazz
* @param methodPattern regex pattern the name of the method must match with (may be {@code null})
*/
public MultiMethodControlFlowPointcut(Class<?> clazz, Pattern methodPattern) {
this(clazz, (String) null);
this.methodPattern = methodPattern;
}
/**
* Subclasses can override this for greater filtering (and performance).
*/
@Override
public boolean matches(Class<?> clazz) {
return true;
}
/**
* Subclasses can override this if it's possible to filter out some candidate classes.
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
return true;
}
@Override
public boolean isRuntime() {
return true;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
this.evaluations++;
for (StackTraceElement element : new Throwable().getStackTrace()) {
if (
element.getClassName().equals(this.clazz.getName()) &&
(this.methodName == null || element.getMethodName().equals(this.methodName)) &&
(this.methodPattern == null || this.methodPattern.matcher(element.getMethodName()).matches())
) {
//System.out.println("Control flow match: " + element.getClassName() + "." + element.getMethodName());
return true;
}
}
return false;
}
/**
* It's useful to know how many times we've fired, for optimization.
*/
public int getEvaluations() {
return this.evaluations;
}
@Override
public ClassFilter getClassFilter() {
return this;
}
@Override
public MethodMatcher getMethodMatcher() {
return this;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof MultiMethodControlFlowPointcut)) {
return false;
}
MultiMethodControlFlowPointcut that = (MultiMethodControlFlowPointcut) other;
return (this.clazz.equals(that.clazz)) &&
ObjectUtils.nullSafeEquals(this.methodName, that.methodName) &&
ObjectUtils.nullSafeEquals(this.methodPattern, that.methodPattern);
}
@Override
public int hashCode() {
int result = clazz.hashCode();
result = 31 * result + (methodName != null ? methodName.hashCode() : 0);
result = 31 * result + (methodPattern != null ? methodPattern.hashCode() : 0);
return result;
}
}
Most of the code is identical to the original, except for the following parts:
@Nullable private Pattern methodPattern
public MultiMethodControlFlowPointcut(Class<?>, Pattern)
equals(..)
and hashCode()
consider methodPattern
public boolean matches(Method, Class<?>, Object...)
considers methodPattern
So if now you instantiate this class with
new MultiMethodControlFlowPointcut(
ControlFlowDemo.class, Pattern.compile("test.*")
)
or
new MultiMethodControlFlowPointcut(
ControlFlowDemo.class, Pattern.compile("test[1-3]")
)
it should do exactly what you want.
Implementation notes:
Instead of a new Pattern
field + constructor, I could have simply treated the existing String
field as a regex pattern by default, but despite being backwards compatible that would slow down exact method name matches. Maybe that would be negligible, I did not measure it.
The regex syntax is inconsistent with AspectJ or Spring AOP syntax which only has simple *
patterns, not full-blown regex ones. But if you are sporting your own custom pointcut class, you can just as well use something more powerful.
Of course, it would be easily possible to extend the implementation to also allow pattern or subclass matching for the Class
part, not just the Method
one. But that would also slow down pointcut matching even more.
Uncomment the log statement in method matches(Method, Class<?>, Object...)
, if you want to see which method in the control flow triggered advice execution.
Update: I have created Spring issue #27187 in order to discuss, if the core class could be either extended or be made more easily extensible in order to avoid duplication.
Update 2: Spring issue #27187 has been implemented for Spring 6.1. Please see Sam Brannen's answer for how to implement this in more recent Spring versions.