Search code examples
javaaopaspectj

Handling void methods in assignment from proceed() in AOP using AspectJ


I am writing a very generic code to capture the return type using around as result = proceed(); followed by return result;.

Some methods are of type void. E.g.

void doPrint() { 
   System.out.println("Doing something"); 
}

How can these methods of return type void be handled in a generic way along with methods returning a value or throwing an exception?

The code I have so far is:

import java.util.Arrays;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import org.aspectj.lang.SoftException;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.SourceLocation;

public aspect LogInjector {
    private pointcut executionJoinPoints(): !within(LogInjector) && execution (* *.*(..));

    Object around(): executionJoinPoints(){
        SourceLocation loc;
        CodeSignature sig;
        
        Class<?> type;
        
        Logger logger;
        
        Object result;
        try {
            loc = thisJoinPointStaticPart.getSourceLocation();
            sig = (CodeSignature) thisJoinPointStaticPart.getSignature();
            
            type = loc.getWithinType();
            if (type == null) type = sig.getDeclaringType();
            
            logger = LogManager.getLogger(type);
            
            result = proceed();
            return result;
        } catch (RuntimeException rte) {
            result = rte;
            throw rte;
        } catch (Throwable t) {
            result = t;
            throw new SoftException(t);
        } finally {
            logger.trace("Source location: {} | Join Point: {} | Signature: {} | Args: {} | Result: {}", loc, thisJoinPointStaticPart, sig, Arrays.deepToString(thisJoinPoint.getArgs()), result);
        }
    }

}

Corrections adapted from this answer by this user.


Solution

  • In the first version of this question I commented on you had forgotten return result; after result = proceed(); which later you have corrected. That was a good first step. But still your code would not compile because:

    • The around() advice does not declare a return type.
    • You cannot throw a Throwable from an around() advice, only a RuntimeException. Hence, you need to wrap each checked Exception, Error or Throwable in something like AspectJ's SoftException or simply in a RuntimeException.

    So please when posting non-compilable code, please mention that fact and also post the compiler errors you get. Do not pretend the code works and there is just a detail problem about void method handling.

    Further problems are:

    • You print the signature which is redundant because you also print the joinpoint already containing the same signature.
    • You determine the declaring type but never print it.
    • You import org.aspectj.ajdt.internal.compiler.ast.Proceed, making the aspect dependent on a non-AspectJ class. More precisely, the class if from AJDT (AspectJ Development Tools), an Eclipse plugin for AspectJ.

    I am also not so sure it is a good idea to overwrite the result by a caught exception and print it as such, but in my following MCVE (which was your job to provide and again you didn't do it just like for the previous question) I left it unchanged.

    Driver application:

    package de.scrum_master.app;
    
    public class Application {
      public static void main(String[] args) {
        Application application = new Application();
        application.doSomething("foo");
        try {
          application.doSomethingSpecial("bar");
        } catch (Exception e) {
          e.printStackTrace();
        }
        application.doSomethingElse("zot");
      }
    
      public int doSomething(String string) {
        System.out.println("Doing something: " + string);
        return 42;
      }
    
      public void doSomethingSpecial(String string) throws Exception {
        throw new Exception("checked exception");
      }
    
      public void doSomethingElse(String string) {
        throw new IllegalArgumentException("runtime exception");
      }
    }
    

    Aspect:

    Please note that I removed Log4J and just print to the console in order to boil down the code to the important part. You can easily add the library again. I did not want to manually add it to my sample program and then try if it was Log4J 1.x or 2.x.

    package de.scrum_master.aspect;
    
    import static java.util.Arrays.deepToString;
    
    import org.aspectj.lang.SoftException;
    import org.aspectj.lang.reflect.CodeSignature;
    import org.aspectj.lang.reflect.SourceLocation;
    
    public aspect LogInjector {
      private pointcut executionJoinPoints() :
        !within(LogInjector) && execution (* *(..));
    
      Object around() : executionJoinPoints() {
        SourceLocation sourceLocation = thisJoinPointStaticPart.getSourceLocation();
        CodeSignature signature = (CodeSignature) thisJoinPointStaticPart.getSignature();
        Class<?> type = sourceLocation.getWithinType();
        if (type == null)
          type = signature.getDeclaringType();
        Object result = null;
        try {
          result = proceed();
          return result;
        } catch (RuntimeException rte) {
          result = rte;
          throw rte;
        } catch (Throwable t) {
          result = t;
          throw new SoftException(t);
        } finally {
          System.out.printf(
            "Source location: %s | Type: %s | Join Point: %s | Args: %s | Result: %s%n",
            sourceLocation, type, thisJoinPoint, deepToString(thisJoinPoint.getArgs()), result
          );
        }
      }
    
    }
    

    As you can see, the aspect simply re-throws runtime exceptions (no need to wrap them into another runtime exception) and wraps other throwables. This is also necessary because otherwise exceptions thrown through a hierarchy of calls would be wrapped multiple times like Russian Matryoshka dolls, which is something we want to avoid.

    Console log:

    Doing something: foo
    Source location: Application.java:15 | Type: class de.scrum_master.app.Application | Join Point: execution(int de.scrum_master.app.Application.doSomething(String)) | Args: [foo] | Result: 42
    Source location: Application.java:20 | Type: class de.scrum_master.app.Application | Join Point: execution(void de.scrum_master.app.Application.doSomethingSpecial(String)) | Args: [bar] | Result: java.lang.Exception: checked exception
    org.aspectj.lang.SoftException
        at de.scrum_master.app.Application.doSomethingSpecial_aroundBody5$advice(Application.java:28)
        at de.scrum_master.app.Application.doSomethingSpecial(Application.java:1)
        at de.scrum_master.app.Application.main_aroundBody0(Application.java:8)
        at de.scrum_master.app.Application.main_aroundBody1$advice(Application.java:21)
        at de.scrum_master.app.Application.main(Application.java:1)
    Caused by: java.lang.Exception: checked exception
        at de.scrum_master.app.Application.doSomethingSpecial_aroundBody4(Application.java:21)
        at de.scrum_master.app.Application.doSomethingSpecial_aroundBody5$advice(Application.java:21)
        ... 4 more
    Source location: Application.java:24 | Type: class de.scrum_master.app.Application | Join Point: execution(void de.scrum_master.app.Application.doSomethingElse(String)) | Args: [zot] | Result: java.lang.IllegalArgumentException: runtime exception
    Source location: Application.java:4 | Type: class de.scrum_master.app.Application | Join Point: execution(void de.scrum_master.app.Application.main(String[])) | Args: [[--command, --option=123]] | Result: java.lang.IllegalArgumentException: runtime exception
    Exception in thread "main" java.lang.IllegalArgumentException: runtime exception
        at de.scrum_master.app.Application.doSomethingElse_aroundBody6(Application.java:25)
        at de.scrum_master.app.Application.doSomethingElse_aroundBody7$advice(Application.java:21)
        at de.scrum_master.app.Application.doSomethingElse(Application.java:1)
        at de.scrum_master.app.Application.main_aroundBody0(Application.java:12)
        at de.scrum_master.app.Application.main_aroundBody1$advice(Application.java:21)
        at de.scrum_master.app.Application.main(Application.java:1)
    

    The log shows that both void and non-void methods are handles nicely and that even the contents of the main() arguments array - I started the sample application with command line parameters --command --option=123 - are nicely printed because I am using Arrays.deepToString(thisJoinPoint.getArgs()) instead of Arrays.toString(thisJoinPoint.getArgs()).


    So, like I said: I don't understand why you think you have problems with void methods while in reality you have a whole bunch of other problems. Did you ever read an AspectJ manual or tutorial and using sample code to start with or are you just using the "trial & error" method?

    P.S.: I would never use the source location in an aspect because an aspect is not a debugger and source code is prone to refactoring anyway. If the code is compiled without debug info, you do not have the source location information in the byte code anyway, so you cannot rely on it. An aspect or logging in general are not replacements for a debugger.


    Update: In comparison to your original question now you don't log both before and after the target method call but only afterwards. In this case a combination of after() returning and after() throwing advice would both be more efficient and simpler to implement because you could avoid exception handling, re-throwing, and the finally block. When in the other question I recommended an around() advice I did so because I saw that your original before() and after() advice each had to determine the same information in order to log it, i.e. twice per method. But if you only need it once, around() is actually unnecessary.


    Update 2: You asked:

    I am running into another problem. By throwing SoftException and RuntimeException the catch blocks are not catching the exceptions supposed to be thrown and caught as per the method signatures in libraries as per normal behaviour.

    If you want the exceptions unchanged, use a combination of after() returning (printing the result) and after() throwing (unchanged exceptions, no need to wrap them) as mentioned above. See the AspectJ manual for more information.

    package de.scrum_master.aspect;
    
    import static java.util.Arrays.deepToString;
    
    import org.aspectj.lang.reflect.CodeSignature;
    import org.aspectj.lang.reflect.SourceLocation;
    
    public aspect LogInjector {
      private pointcut executionJoinPoints() :
        !within(LogInjector) && execution (* *(..));
    
      after() returning (Object result): executionJoinPoints() {
        SourceLocation sourceLocation = thisJoinPointStaticPart.getSourceLocation();
        CodeSignature signature = (CodeSignature) thisJoinPointStaticPart.getSignature();
        Class<?> type = sourceLocation.getWithinType();
        if (type == null)
          type = signature.getDeclaringType();
        System.out.printf("Source location: %s | Type: %s | Join Point: %s | Args: %s | Result: %s%n",
          sourceLocation, type, thisJoinPoint, deepToString(thisJoinPoint.getArgs()), result
        );
      }
    
      after() throwing (Throwable error): executionJoinPoints() {
        SourceLocation sourceLocation = thisJoinPointStaticPart.getSourceLocation();
        CodeSignature signature = (CodeSignature) thisJoinPointStaticPart.getSignature();
        Class<?> type = sourceLocation.getWithinType();
        if (type == null)
          type = signature.getDeclaringType();
        System.out.printf("Source location: %s | Type: %s | Join Point: %s | Args: %s | Error: %s%n",
          sourceLocation, type, thisJoinPoint, deepToString(thisJoinPoint.getArgs()), error
        );
      }
    
    }
    

    The console log will change to:

    Doing something: foo
    Source location: Application.java:15 | Type: class de.scrum_master.app.Application | Join Point: execution(int de.scrum_master.app.Application.doSomething(String)) | Args: [foo] | Result: 42
    Source location: Application.java:20 | Type: class de.scrum_master.app.Application | Join Point: execution(void de.scrum_master.app.Application.doSomethingSpecial(String)) | Args: [bar] | Error: java.lang.Exception: checked exception
    java.lang.Exception: checked exception
        at de.scrum_master.app.Application.doSomethingSpecial(Application.java:21)
        at de.scrum_master.app.Application.main(Application.java:8)
    Source location: Application.java:24 | Type: class de.scrum_master.app.Application | Join Point: execution(void de.scrum_master.app.Application.doSomethingElse(String)) | Args: [zot] | Error: java.lang.IllegalArgumentException: runtime exception
    Source location: Application.java:4 | Type: class de.scrum_master.app.Application | Join Point: execution(void de.scrum_master.app.Application.main(String[])) | Args: [[--command, --option=123]] | Error: java.lang.IllegalArgumentException: runtime exception
    Exception in thread "main" java.lang.IllegalArgumentException: runtime exception
        at de.scrum_master.app.Application.doSomethingElse(Application.java:25)
        at de.scrum_master.app.Application.main(Application.java:12)