Search code examples
javaspringaop

How to specify multiple methods for Spring ControlFlowPointcut?


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");


Solution

  • 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

    • to switch to native AspectJ and use the existing cflow and cflowbelow pointcuts there, or
    • to copy and modify the source code. I was curious, so I did that for you. I tried to extend it at first, but some members which should be protected 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:

    • new field @Nullable private Pattern methodPattern
    • new constructor 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.