3

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 
4evertoblerone
  • 95
  • 1
  • 3
  • 12

2 Answers2

10

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)
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Oh, I did not know about that extra pointcut. Is it available in all AspectJ implementations though? As far as I know, for example, SpringAspectJ implementation does not allow to use the handler, as can be seen [there(Spring-pointcut-designators)](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html#aop-pointcuts-designators) – M. Prokhorov Feb 07 '17 at 15:25
  • I know, but this question is about AspectJ, not about Spring AOP. The latter is just "AOP lite", and the manual section you linked to a mentions in the grey box "Other pointcut types" that AspectJ supports `handler()` and Spring AOP does not. But the good news is: You can also configure Spring to use full [AspectJ via load-time weaving](https://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html#aop-using-aspectj). Then you can also use `handler()`. – kriegaex Feb 07 '17 at 15:34
  • yes, I already edited my answer into what I think I wanted to say, explicitly pointing out that it was not about "full AspectJ". Having only previously worked with spring aop flavor bit me there, it seems. – M. Prokhorov Feb 07 '17 at 15:37
  • Is there a way to get MethodSignature object from JoinPoint defined in 'logCaughtException' so i could be able to get method params and annotations or any other way to get them? – 4evertoblerone Feb 07 '17 at 16:00
  • 1
    This seemingly simple flollow-up question does not have a short answer and leads us somewhat off-topic. I took some time to answer it in two ways anyway. Please see my update. – kriegaex Feb 07 '17 at 17:50
  • Thanks for the informative answer. Very helpful. I have to go a step further, I want to weave code only when the Exception parameter variable is annotated with some annotation, like “ catch (@AutoLog Exception e ) { //weave code here... } ”. Do you know how to do that? I looked up many sources and didn't find a working way. – Jacob Wu Oct 07 '18 at 10:43
  • Sorry for not answering, I am quite busy these days. @JacobWu, you are describing a very unusual thus special case. I cannot answer without trying. Please create a new question and feel free to point me to it. It is definitely out of scope here. – kriegaex Nov 23 '18 at 11:25
0

Depending on actual implementation of AspectJ features, it is possible. See kriegaex's answer for how to do it.

However, when using AspectJ features in certain scenarios, you might find that not all pointcut definitions are supported. One example of this is Spring Framework AOP, which only supports a subset of AspectJ features . What you can then do is have two methods: a non-exposed but instrumentable method that doesn't catch exceptions (the one you will instrument), and exposed method that does catches. Like this:

protected void divideByZeroNoCatch(int arg) {
  int r = arg / 0;
}

public void divideByZeroSafe(int arg) {
  try {
    divideByZeroNoCatch(arg);
  } catch(ArithmeticException ae) {
    logException(ae);
  }
}

After that, you can pointcut on divideByZeroNoCatch, which will give you ability to do your AfterThrowing. Obviously, the pointcut will have to change a little. And this will not work if your implementation of AspectJ does not support instrumenting non-public methods.

Community
  • 1
  • 1
M. Prokhorov
  • 3,894
  • 25
  • 39
  • The answer is incorrect. Check out [mine](http://stackoverflow.com/a/42093318/1082681). – kriegaex Feb 07 '17 at 15:11
  • @kriegaex, thanks for pointing this out. I will edit my answer with regards to yours. – M. Prokhorov Feb 07 '17 at 15:26
  • Sorry, but your edit is also false, as I said in another comment under my answer. There is only one AspectJ implementation. You are talking about Spring AOP, but Spring AOP != AspectJ. They are different things and the former only shares the basic syntax and is a small subset of the latter, implemented in a totally different way (using dynamic proxies), whereas AspectJ does not need/use proxies but instruments byte code directly. – kriegaex Feb 07 '17 at 15:37
  • @kriegaex, Ok, now I'm really stumped. Should I just remove mine then? – M. Prokhorov Feb 07 '17 at 15:38
  • 1
    No, don't remove it. (And I did not mean to be rude, I was just trying to clarify the situation.) I think our discussion explains a few things to other readers. The fact that maybe my answer will be accepted does not invalidate yours, correct or not. The difference between Spring AOP and AspectJ is unclear to most of the people only knowing one of them. For Spring AOP your answer would have been correct. :-) – kriegaex Feb 07 '17 at 15:41