javagradleaspectjfreefair-aspectj

How can I setup AspectJ for a non-Spring Java 11 app built with Gradle?


For the life of me, I can't figure out how to get some AspectJ advice to run.

Current Setup

Attempted Config (one attempt among many)

build.gradle:

plugins {
    id "io.freefair.aspectj" version "8.3"
}
dependencies {
    // https://mvnrepository.com/artifact/org.aspectj/aspectjweaver
    implementation group: 'org.aspectj', name: 'aspectjweaver', version: '1.9.20.1'
    // https://mvnrepository.com/artifact/org.aspectj/aspectjrt
    implementation group: 'org.aspectj', name: 'aspectjrt', version: '1.9.20.1'
}

Sample Aspect

LogExecutionTime.java

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {}

LogExecutionTimeAspect.java

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class LogExecutionTimeAspect {
    @Around("@annotation(LogExecutionTime)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object proceed = pjp.proceed();
        long executionTime = System.currentTimeMillis() - start;
        System.out.println(pjp.getSignature() + " executed in " + executionTime + "ms");
        return proceed;
    }
}

TestClass.java

public class TestClass {
    @LogExecutionTime
    public String hello() {
        return "hello";
    }
}

I've called new TestClass().hello() in both a unit test (JUnit 5) and when running our application locally (Appengine Web App). Everything compiles and runs, but the execution time isn't getting printed out. I've tried debugging and putting a break point inside the LogExecutionTimeAspect::around method, but it was never reached.

I feel like it shouldn't be this complicated, but I can't seem to figure it out.

Update

I have a basic working project here for anyone interested: https://github.com/MuffinTheMan/gradle-aspectj


Solution

  • For me, this works. Did you really forget the java plugin, or did you post an incomplete Gradle build file? What I cannot know is if your annotation is in the exact same package as the aspect, otherwise you would need a fully qualified class name in the @annotation() pointcut.

    plugins {
      id 'java'
      id "io.freefair.aspectj" version "8.3"
    }
    
    repositories {
      mavenLocal()
      mavenCentral()
    }
    
    dependencies {
      implementation 'org.aspectj:aspectjrt:1.9.20.1'
    }
    
    package de.scrum_master.stackoverflow.q77174865;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface LogExecutionTime {}
    
    package de.scrum_master.stackoverflow.q77174865;
    
    public class TestClass {
      @LogExecutionTime
      public String hello() {
        return "hello";
      }
    
      public static void main(String[] args) {
        new TestClass().hello();
      }
    }
    
    package de.scrum_master.stackoverflow.q77174865;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    
    @Aspect
    public class LogExecutionTimeAspect {
      @Around("@annotation(LogExecutionTime)")
      public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object proceed = pjp.proceed();
        long executionTime = System.currentTimeMillis() - start;
        System.out.println(pjp.getSignature() + " executed in " + executionTime + "ms");
        return proceed;
      }
    }
    

    Running from my IDE IntelliJ IDEA, the console log looks as follows:

    13:24:24: Executing ':TestClass.main()'...
    
    > Task :compileJava NO-SOURCE
    > Task :compileAspectj
    > Task :processResources NO-SOURCE
    > Task :classes
    
    > Task :TestClass.main()
    String de.scrum_master.stackoverflow.q77174865.TestClass.hello() executed in 0ms
    String de.scrum_master.stackoverflow.q77174865.TestClass.hello() executed in 2ms
    (...)
    

    Of course, you see two log messages, because your pointcut captures both call() and execution() joinpoints. You can fix that as follows:

    package de.scrum_master.stackoverflow.q77174865;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    
    @Aspect
    public class LogExecutionTimeAspect {
      @Around("@annotation(LogExecutionTime) && execution(* *(..))")
      public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try {
          return pjp.proceed();
        } finally {
          System.out.println(pjp.getSignature() + " executed in " + (System.currentTimeMillis() - start) + "ms");
        }
      }
    }
    

    One thing you maybe forgot is that for Freefair, the default source directory is src/man/aspectj. For me, it looks like this:

    IDE screenshot showing directory tree

    The default will probably change to src/main/java in version 8.4, see GitHub issue #881.


    Update 2023-10-10: Freefair 8.4 has been published and now defaults to src/main/java, i.e. it works as expected with a standard directory layout, just like with AspectJ Maven.