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

    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:

    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:


    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.