Search code examples
javaspring-bootaopaspectjspring-aop

Spring AOP - Pointcut applying to method only when joinPoint.proceed is invoked from a lambda


(Title isn't the best, but I couldn't find a good way to phrase the following problem)

Given

@Aspect
@Component
class MyAspect {
  @Autowired private MyService service;

  @Around("@target(org.springframework.ws.server.endpoint.annotation.Endpoint)")
  public Object aroundEndpoint(ProceedingJoinPoint joinPoint) {
    return service.around(joinPoint::proceed);
  }

  @Around("@target(org.springframework.stereotype.Service)") // And some other expressions to exclude `MyService`
  public Object aroundService(ProceedingJoinPoint joinPoint) throws Throwable {
    // ...
  }
}

@Service
class MyService {
  // My own Callable<T> with Throwable instead of Exception
  public Object around(Callable<?> callable) throws Throwable {
    // Do stuff
    Object returnValue = callable.call();
    // Do stuff
    return returnValue;
  }
}

When an endpoint method is invoked, it is intercepted by aroundEndpoint. If I were to invoke joinPoint.proceed() right away, everything would work as expected. However, if I pass it as a method reference (or a lambda) into MyService.around, and then invoke it, it is matched against my service pointcut and my around service advice is applied to it.

I did some debugging, and here's what I see: in AspectJExpressionPointcut.matches, thisObject and targetObject refer to my endpoint in the former case, but refer to my service in the latter case. This is probably because it uses ExposeInvocationInterceptor.currentInvocation(), and doing another method call messes with it.

Is this a bug? Some limitation of the proxy-based approach? Or do I have to simply inline MyService.aroundService?


Solution

  • I reproduced your problem and also compared a similar setup in plain Java + AspectJ (i.e. without Spring or Spring AOP, only using two Spring annotations used in aspect pointcuts). There the problem does not occur. It is something specific to Spring AOP, that much is sure.

    Now Spring uses AspectJ's pointcut matching in combination with its own AOP framework based on proxies and delegation. Somewhere in there this edge case must mess up the status of Spring aspect matching, causing the behaviour you see. I have not debugged into it so far, but from what I see now I would suggest to create an issue and see what the maintainers say about it.

    Here is my AspectJ MCVE proving that the problem does not occur there. BTW, I had to rename the package aspect to aop because in AspectJ aspect is a reserved keyword. But I also renamed it in the Spring project in order to make sure it is unrelated to the problem at hand, and it is unrelated.

    package aop.mcve;
    
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class MyController {
      public void controllerMethod() {}
    }
    
    package aop.mcve;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class MyService {
      public Object delegateTo(MyAspect.Callable<?> callable) throws Throwable {
        return callable.call();
      }
    
      public void serviceMethod() {}
    }
    
    package aop.mcve;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    
    @Aspect
    public class MyAspect {
      private final MyService myService = new MyService();
    
      @Pointcut("within(aop.mcve..*) && !within(MyAspect) && execution(* *(..))")
      public void inDomain() {}
    
      @Pointcut("@target(org.springframework.stereotype.Service)")
      public void inService() {}
    
      @Pointcut("execution(* aop.mcve.MyService.*(..))")
      public void inMyService() {}
    
      @Pointcut("@target(org.springframework.web.bind.annotation.RestController)")
      public void inController() {}
    
      @Around("inDomain() && inController()")
      public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("aroundController -> " + joinPoint);
        return myService.delegateTo(joinPoint::proceed);
      }
    
      @Around("inDomain() && inService() && !inMyService()")
      public Object aroundService(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("aroundService -> " + joinPoint);
        System.out.println("You should never see this message!");
        return joinPoint.proceed();
      }
    
      public interface Callable<T> {
        T call() throws Throwable;
      }
    }
    
    package aop.mcve;
    
    public class AspectMcveApplication {
      public static void main(String[] args) throws Throwable {
        new MyService().serviceMethod();
        new MyController().controllerMethod();
      }
    }
    

    The console log:

    aroundController -> execution(void aop.mcve.MyController.controllerMethod())
    

    As you can see, the advice method aroundService(..) does not get triggered like in Spring AOP.


    Update: I modified your MCVE in order to make it runnable with both Spring AOP and AspectJ, it automatically detects AspectJ's load-time weaver when active. I sent you this pull request.