Search code examples
javaaopaspectjpointcutaspects

AspectJ Handling of Multiple Matching Advices


I am using AspectJ in Java to log the calls to some methods. I've looked online but couldn't manage to find an answer to this:

What happens when two @Around advices match for a method?

Specifically, I am using two @Around advices, like this:

@Around("condition1() && condition2() && condition3()")
public Object around(ProceedingJoinPoint point) {
    return around(point, null);
}

@Around("condition1() && condition2() && condition3() && args(request)")
public Object around(ProceedingJoinPoint point, Object request) {
    ...
    result = (Result) point.proceed();
    ...
}

Will this result in point.proceed() being called twice (having the actual method called twice) if both of these advices match?


Solution

  • Your approach is highly problematic because you manually call one advice from another one. This is not how AOP should be applied. Please let AspectJ decide which advices to execute based on their respective pointcuts. The way you delegate from one advice to another you could even call an advice which would not match by itself. Example in plain AspectJ without Spring (works the same in Spring AOP, though):

    Java driver application:

    package de.scrum_master.app;
    
    public class Application {
        private static void doSomething() {
            System.out.println("Doing something");
        }
    
        public static void main(String[] args) {
            doSomething();
        }
    }
    

    Aspect:

    package de.scrum_master.aspect;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    
    @Aspect
    public class MyBogusAspect {
        @Around("execution(* doSomething(..))")
        public Object matchingAdvice(ProceedingJoinPoint thisJoinPoint) {
            System.out.println("matching advice called on joinpoint " + thisJoinPoint);
            return nonMatchingAdvice(thisJoinPoint);
        }
    
        @Around("execution(* doSomethingElse(..))")
        public Object nonMatchingAdvice(ProceedingJoinPoint thisJoinPoint) {
            System.out.println("non-matching advice called on joinpoint " + thisJoinPoint);
            return thisJoinPoint.proceed();
        }
    }
    

    Console log:

    matching advice called on joinpoint execution(void de.scrum_master.app.Application.doSomething())
    non-matching advice called on joinpoint execution(void de.scrum_master.app.Application.doSomething())
    Doing something
    

    Can you see how unhealthy your approach is? An advice which otherwise would not match is called by a matching one. This yields some really unexpected behaviour IMO. Please don't do it!!!

    Now as for your original question about multiple matching advice, this is how you should do it:

    Modified aspect:

    package de.scrum_master.aspect;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    
    @Aspect
    public class MyBetterAspect {
        @Around("execution(* doSomething(..))")
        public Object matchingAdvice(ProceedingJoinPoint thisJoinPoint) {
            System.out.println(">>> matching advice on " + thisJoinPoint);
            Object result = thisJoinPoint.proceed();
            System.out.println("<<< matching advice on " + thisJoinPoint);
            return result;
        }
    
        @Around("execution(* doSomething(..))")
        public Object anotherMatchingAdvice(ProceedingJoinPoint thisJoinPoint) {
            System.out.println(">>> another matching advice on " + thisJoinPoint);
            Object result = thisJoinPoint.proceed();
            System.out.println("<<< another matching advice on " + thisJoinPoint);
            return result;
        }
    }
    

    New console log:

    >>> matching advice on execution(void de.scrum_master.app.Application.doSomething())
    >>> another matching advice on execution(void de.scrum_master.app.Application.doSomething())
    Doing something
    <<< another matching advice on execution(void de.scrum_master.app.Application.doSomething())
    <<< matching advice on execution(void de.scrum_master.app.Application.doSomething())
    

    As you can see, AspectJ or Spring AOP wrap multiple matching advice like onion skins around joinpoints and only the innermost proceed() calls the actual joinpoint while the outer layers call the inner ones, making sure that each joinpoint is executed only once. There is no need for you trying to be smarter than the AOP framework, possibly causing damage (see my first example).

    One more thing: If multiple aspects have matching pointcuts, you can influence their order of execution via @DeclarePrecedence in AspectJ, but within a single aspect you have no influence on the execution order or at least you should not rely on it. In Spring AOP you can use the @Order annotation in order to determine aspect precedence, but the order is also undefined for multiple advice from the same aspect, see also the Spring manual.


    Update 2016-02-28, 18:30 CET, after some discussion in comments:

    Okay, we extend the driver class a little bit so we can test some more:

    package de.scrum_master.app;
    
    public class Application {
        private static void doSomething() {
            System.out.println("Doing something");
        }
    
        private static String doSomethingElse(String text) {
            System.out.println("Doing something else");
            return text;
        }
    
        private static int doAnotherThing(int i, int j, int k) {
            System.out.println("Doing another thing");
            return (i + j) * k;
        }
    
        public static void main(String[] args) {
            doSomething();
            doSomethingElse("foo");
            doAnotherThing(11, 22, 33);
        }
    }
    

    Now, binding the first parameter in AspectJ is as easy as args(request, ..) which works for one or more parameters. The only exception is zero parameters, in which case the pointcut would not fire. So either I end up with something similar to what you did:

    package de.scrum_master.aspect;
    
    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 BoundFirstParameterAspect {
        @Pointcut("execution(* do*(..))")
        public static void myPointcut() {}
    
        @Around("myPointcut()")
        public Object matchingAdvice(ProceedingJoinPoint thisJoinPoint) {
            return anotherMatchingAdvice(thisJoinPoint, null);
        }
    
        @Around("myPointcut() && args(request, ..)")
        public Object anotherMatchingAdvice(ProceedingJoinPoint thisJoinPoint, Object request) {
            System.out.println(">>> another matching advice on " + thisJoinPoint);
            Object result = thisJoinPoint.proceed();
            System.out.println("<<< another matching advice on " + thisJoinPoint);
            return result;
        }
    }
    

    Which is makes the same advice fire twice and thus causes an overhead, even though the original method is only called once, but you can see the overhead in the log:

    >>> another matching advice on execution(void de.scrum_master.app.Application.doSomething())
    Doing something
    <<< another matching advice on execution(void de.scrum_master.app.Application.doSomething())
    >>> another matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
    >>> another matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
    Doing something else
    <<< another matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
    <<< another matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
    >>> another matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
    >>> another matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
    Doing another thing
    <<< another matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
    <<< another matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
    

    You can easily recognise how double advices are fired for each joinpoint.

    Alternatively, you can bind the parameter during runtime, which is not very elegant and incurs a little runtime penalty, but works perfectly well:

    package de.scrum_master.aspect;
    
    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 BoundFirstParameterAspect {
        @Pointcut("execution(* do*(..))")
        public static void myPointcut() {}
    
        @Around("myPointcut()")
        public Object matchingAdvice(ProceedingJoinPoint thisJoinPoint) {
            System.out.println(">>> matching advice on " + thisJoinPoint);
            Object[] args = thisJoinPoint.getArgs();
            Object request =  args.length > 0 ? args[0] : null;
            System.out.println("First parameter = " + request);
            Object result = thisJoinPoint.proceed();
            System.out.println("<<< matching advice on " + thisJoinPoint);
            return result;
        }
    }
    

    This avoids double advice execution as well as code duplication and yields the following console output:

    >>> matching advice on execution(void de.scrum_master.app.Application.doSomething())
    First parameter = null
    Doing something
    <<< matching advice on execution(void de.scrum_master.app.Application.doSomething())
    >>> matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
    First parameter = foo
    Doing something else
    <<< matching advice on execution(String de.scrum_master.app.Application.doSomethingElse(String))
    >>> matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
    First parameter = 11
    Doing another thing
    <<< matching advice on execution(int de.scrum_master.app.Application.doAnotherThing(int, int, int))
    

    Last, but not least, you can have two slightly different pointcuts - one with empty args() and one with args(request, ..) - both of which can delegate parameter handling, logging and exception handling to a helper method in order to avoid duplication, as I said in one of my comments:

    package de.scrum_master.aspect;
    
    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 BoundFirstParameterAspect {
        @Pointcut("execution(* do*(..))")
        public static void myPointcut() {}
    
        @Around("myPointcut() && args()")
        public Object myAdvice(ProceedingJoinPoint thisJoinPoint) {
            return myAdviceHelper(thisJoinPoint, null);
        }
    
        @Around("myPointcut() && args(request, ..)")
        public Object myAdviceWithParams(ProceedingJoinPoint thisJoinPoint, Object request) {
            return myAdviceHelper(thisJoinPoint, request);
        }
    
        private Object myAdviceHelper(ProceedingJoinPoint thisJoinPoint, Object request) {
            System.out.println(">>> matching advice on " + thisJoinPoint);
            System.out.println("First parameter = " + request);
            Object result = thisJoinPoint.proceed();
            System.out.println("<<< matching advice on " + thisJoinPoint);
            return result;
        }
    }
    

    The console log should be exactly the same as the previous one.


    Update 2:

    Well, I just realised that the empty args() trick would also apply to your original idea and avoid double execution as well as the helper method:

    package de.scrum_master.aspect;
    
    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 BoundFirstParameterAspect {
        @Pointcut("execution(* do*(..))")
        public static void myPointcut() {}
    
        @Around("myPointcut() && args()")
        public Object myAdvice(ProceedingJoinPoint thisJoinPoint) {
            return myAdviceWithParams(thisJoinPoint, null);
        }
    
        @Around("myPointcut() && args(request, ..)")
        public Object myAdviceWithParams(ProceedingJoinPoint thisJoinPoint, Object request) {
            System.out.println(">>> matching advice on " + thisJoinPoint);
            System.out.println("First parameter = " + request);
            Object result = thisJoinPoint.proceed();
            System.out.println("<<< matching advice on " + thisJoinPoint);
            return result;
        }
    }
    

    This is acceptable as well as elegant because it does not generate byte code twice per joinpoint. The two pointcuts are mutually exclusive, so this is a good thing to do. I recommend this solution.