Search code examples
javaexceptionaopaspectjaspects

How to intercept method which handles its own exceptions using AspectJ


I'm trying add some monitoring when some specific exception occurs. For example, if I have an aspect like this:

@Aspect
public class LogAspect {

  @AfterThrowing(value = "execution(* *(..))", throwing = "e")
  public void log(JoinPoint joinPoint, Throwable e){
    System.out.println("Some logging stuff");
  }
}

And test class:

 public class Example {


  public void divideByZeroWithCatch(){
    try{
      int a = 5/0;
    }
    catch (ArithmeticException e){
      System.out.println("Can not divide by zero");
    }
  }

  public void divideByZeroWithNoCatch(){
    int b = 5/0;
  }

  public static void main (String [] args){
    Example e = new Example();
    System.out.println("***** Calling method with catch block *****");
    e.divideByZeroWithCatch();
    System.out.println("***** Calling method without catch block *****");
    e.divideByZeroWithNoCatch();
  }
}

As an output i will get:

***** Calling method with catch block *****
Can not divide by zero
***** Calling method without catch block *****
Some logging stuff

I was wondering if there is way for me to intercept method execution just after throwing exception, do something in my advice and continue with executing code in corresponding catch block? So that if i call divideByZeroWithCatch()i can get:

Some logging stuff
Can not divide by zero 

Solution

  • Yes, you can. You need a handler() pointcut:

    package de.scrum_master.aspect;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    @Aspect
    public class LogAspect {
        @AfterThrowing(value = "execution(* *(..))", throwing = "e")
        public void log(JoinPoint thisJoinPoint, Throwable e) {
            System.out.println(thisJoinPoint + " -> " + e);
        }
    
        @Before("handler(*) && args(e)")
        public void logCaughtException(JoinPoint thisJoinPoint, Exception e) {
            System.out.println(thisJoinPoint + " -> " + e);
        }
    }
    

    Log output, assuming class Example is in package de.scrum_master.app:

    ***** Calling method with catch block *****
    handler(catch(ArithmeticException)) -> java.lang.ArithmeticException: / by zero
    Can not divide by zero
    ***** Calling method without catch block *****
    execution(void de.scrum_master.app.Example.divideByZeroWithNoCatch()) -> java.lang.ArithmeticException: / by zero
    execution(void de.scrum_master.app.Example.main(String[])) -> java.lang.ArithmeticException: / by zero
    Exception in thread "main" java.lang.ArithmeticException: / by zero
        at de.scrum_master.app.Example.divideByZeroWithNoCatch(Example.java:13)
        at de.scrum_master.app.Example.main(Example.java:21)
    

    Update: If you want to know where the exception handler is located, there is a simple way: use the enclosing joinpoint's static part. You can also get information about parameter names and types etc. Just use code completion in order to see which methods are available.

    @Before("handler(*) && args(e)")
    public void logCaughtException(
        JoinPoint thisJoinPoint,
        JoinPoint.EnclosingStaticPart thisEnclosingJoinPointStaticPart,
        Exception e
    ) {
        // Exception handler
        System.out.println(thisJoinPoint.getSignature() + " -> " + e);
    
        // Method signature + parameter types/names
        MethodSignature methodSignature = (MethodSignature) thisEnclosingJoinPointStaticPart.getSignature();
        System.out.println("    " + methodSignature);
        Class<?>[] paramTypes = methodSignature.getParameterTypes();
        String[] paramNames = methodSignature.getParameterNames();
        for (int i = 0; i < paramNames.length; i++)
            System.out.println("      " + paramTypes[i].getName() + " " + paramNames[i]);
    
        // Method annotations - attention, reflection!
        Method method = methodSignature.getMethod();
        for (Annotation annotation: method.getAnnotations())
            System.out.println("    " + annotation);
    }
    

    Now update your code like this:

    package de.scrum_master.app;
    
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
        int id();
        String name();
        String remark();
    }
    
    package de.scrum_master.app;
    
    public class Example {
        @MyAnnotation(id = 11, name = "John", remark = "my best friend")
        public void divideByZeroWithCatch(int dividend, String someText) {
            try {
                int a = 5 / 0;
            } catch (ArithmeticException e) {
                System.out.println("Can not divide by zero");
            }
        }
    
        public void divideByZeroWithNoCatch() {
            int b = 5 / 0;
        }
    
        public static void main(String[] args) {
            Example e = new Example();
            System.out.println("***** Calling method with catch block *****");
            e.divideByZeroWithCatch(123, "Hello world!");
            System.out.println("***** Calling method without catch block *****");
            e.divideByZeroWithNoCatch();
        }
    }
    

    Then the console log says:

    ***** Calling method with catch block *****
    catch(ArithmeticException) -> java.lang.ArithmeticException: / by zero
        void de.scrum_master.app.Example.divideByZeroWithCatch(int, String)
          int dividend
          java.lang.String someText
        @de.scrum_master.app.MyAnnotation(id=11, name=John, remark=my best friend)
    Can not divide by zero
    ***** Calling method without catch block *****
    execution(void de.scrum_master.app.Example.divideByZeroWithNoCatch()) -> java.lang.ArithmeticException: / by zero
    execution(void de.scrum_master.app.Example.main(String[])) -> java.lang.ArithmeticException: / by zero
    Exception in thread "main" java.lang.ArithmeticException: / by zero
        at de.scrum_master.app.Example.divideByZeroWithNoCatch(Example.java:14)
        at de.scrum_master.app.Example.main(Example.java:22)
    

    If that is good enough for you, then you are fine. But beware, the static part is not the full joinpoint, so you cannot access parameter values from there. In order to do that you have to do manual bookkeeping. And this is possibly expensive and can slow down your application. But for what it is worth, I show you how to do it:

    package de.scrum_master.aspect;
    
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Method;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.reflect.MethodSignature;
    
    @Aspect
    public class LogAspect {
        private ThreadLocal<JoinPoint> enclosingJoinPoint;
    
        @AfterThrowing(value = "execution(* *(..))", throwing = "e")
        public void log(JoinPoint thisJoinPoint, Throwable e) {
            System.out.println(thisJoinPoint + " -> " + e);
        }
    
        @Before("execution(* *(..)) && within(de.scrum_master.app..*)")
        public void recordJoinPoint(JoinPoint thisJoinPoint) {
            if (enclosingJoinPoint == null)
                enclosingJoinPoint = ThreadLocal.withInitial(() -> thisJoinPoint);
            else
                enclosingJoinPoint.set(thisJoinPoint);
        }
    
        @Before("handler(*) && args(e)")
        public void logCaughtException(JoinPoint thisJoinPoint, Exception e) {
            // Exception handler
            System.out.println(thisJoinPoint + " -> " + e);
    
            // Method signature + parameter types/names
            JoinPoint enclosingJP = enclosingJoinPoint.get();
            MethodSignature methodSignature = (MethodSignature) enclosingJP.getSignature();
            System.out.println("    " + methodSignature);
            Class<?>[] paramTypes = methodSignature.getParameterTypes();
            String[] paramNames = methodSignature.getParameterNames();
            Object[] paramValues = enclosingJP.getArgs();
            for (int i = 0; i < paramNames.length; i++)
                System.out.println("      " + paramTypes[i].getName() + " " + paramNames[i] + " = " + paramValues[i]);
    
            // Target object upon which method is executed
            System.out.println("    " + enclosingJP.getTarget());
    
            // Method annotations - attention, reflection!
            Method method = methodSignature.getMethod();
            for (Annotation annotation: method.getAnnotations())
                System.out.println("    " + annotation);
        }
    }
    

    Why do we need a ThreadLocal member for the joinpoint bookkeeping? Well, because obviously we would get into problems in multi-threaded applications otherwise.

    Now the console log says:

    ***** Calling method with catch block *****
    handler(catch(ArithmeticException)) -> java.lang.ArithmeticException: / by zero
        void de.scrum_master.app.Example.divideByZeroWithCatch(int, String)
          int dividend = 123
          java.lang.String someText = Hello world!
        de.scrum_master.app.Example@4783da3f
        @de.scrum_master.app.MyAnnotation(id=11, name=John, remark=my best friend)
    Can not divide by zero
    ***** Calling method without catch block *****
    execution(void de.scrum_master.app.Example.divideByZeroWithNoCatch()) -> java.lang.ArithmeticException: / by zero
    execution(void de.scrum_master.app.Example.main(String[])) -> java.lang.ArithmeticException: / by zero
    Exception in thread "main" java.lang.ArithmeticException: / by zero
        at de.scrum_master.app.Example.divideByZeroWithNoCatch(Example.java:14)
        at de.scrum_master.app.Example.main(Example.java:22)