Hot questions for Using AspectJ in gradle plugin

Top Java Programmings / AspectJ / gradle plugin

Question:

I have a very simple AspectJ aspect (using @AspectJ) which just prints out a log message. My goal is to advice code in my android application. Now this aspects works perfectly fine as long as I have the aspect class itself in my applications source-code. Once I move the aspect into a different module (either java -> .jar or android lib -> .aar) I get the following runtime exception when running the adviced code in my application:

java.lang.NoSuchMethodError: com.xxx.xxx.TraceAspect.aspectOf

Basically my structure is like this:

Root
 + app (com.android.application)
   - MainActivity (with annotation to be adviced)
 + library (android-library)
   - TraceAspect (aspect definition)

From the ajc compiler, I can see that the ajc compiler picks up my classes and advices them correctly, so I really don't know why it works as long as I have the @AspectJ class in my sourcecode, but stops working once I move it to a jar archive.

I am using gradle. Buildscript for my app is super simple. I followed the instructions in http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/

import com.android.build.gradle.LibraryPlugin
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.android.tools.build:gradle:0.12.+'
    classpath 'org.aspectj:aspectjtools:1.8.1'
  }
}

apply plugin: 'com.android.application'

repositories {
  mavenCentral()
}

dependencies {
  compile 'org.aspectj:aspectjrt:1.8.1'
  compile project (':library')
}


android.applicationVariants.all { variant ->
    AppPlugin plugin = project.plugins.getPlugin(AppPlugin)
    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.5",
                         "-XnoInline",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", plugin.project.android.bootClasspath.join(File.pathSeparator)]

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler)

        def log = project.logger
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}

Not sure if important, but just in case, the code of my aspect:

@Aspect
public class TraceAspect {
  private static final String POINTCUT_METHOD = "execution(@com.xxx.TraceAspect * *(..))";

  @Pointcut(POINTCUT_METHOD)
  public void annotatedMethod() {}

  @Around("annotatedMethod()")
  public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("Aspect works...");
    return joinPoint.proceed();
  }
}

Classpath

I also checked the javaCompile.classPath and it correctly contains both the library-classes.jar and my app-classes.jar. Adding -log file to the ajc tasks also shows that files are correctly weaved.

Any ideas?

Minimal example to reproduce this problem

https://github.com/fschoellhammer/test-aspectj


Answer:

The message implies that the aspect file has not gone through the aspectj weaver. The weaver would be responsible for adding the aspectOf() method. Although your annotation style aspects will compile fine with javac, they must be 'finished off' by aspectj at some point to introduce the infrastructure methods that support weaving. If you were load-time weaving this is done as the aspects are loaded but if you are compile time or post-compile time weaving then you need to get them to ajc some other way. If you have a library built like this:

javac MyAspect.java
jar -cvMf code.jar MyAspect.class

then you'd need to get that jar woven to 'complete' the aspects:

ajc -inpath code.jar -outjar myfinishedcode.jar

Or you could just use ajc instead of javac for the initial step

ajc MyAspect.java

Or you could do it at the point the aspects are being applied to your other code:

ajc <myAppSourceFiles> -inpath myaspects.jar 

By including myaspects.jar on the inpath, any aspect classes in there will be 'finished off' as part of this compile step and the finished versions put alongside your compiled application source files. Note this is different to if you used the aspect path:

ajc <myAppSourceFiles> -aspectpath myaspects.jar

Here the aspects on the aspect path are applied to your code but they are only loaded from there, they are not finished off and so you wouldn't get the finished versions alongside your compiled application source files.

Question:

I am trying to use an AspectJ Annotation that is in a Library, that I am pulling into my project. My project uses Gradle, so I am attempting to use FreeFair AspectJ Gradle Plugin.

I need to be able to set the AspectJ -aspectpath argument, to the Library Dependency that Gradle is pulling in.

FreeFair, does not seem to have much Documentation, mainly just Sample Code.

In their sample code, I see that I can use this to set the -aspectpath to a local "project":

aspect project(":aspectj:aspect")

Does anyone know how to set the -aspectpath to an external library dependency?

I created an example Project and put it on GitHub: freefair-aspectpath-external-library.

  • Note: I am using io.freefair.gradle:aspectj-plugin version 2.9.5 because my project is stuck using Gradle version 4.10.3.

Update: I have created a bug for this: https://github.com/freefair/gradle-plugins/issues/46


Answer:

Thanks to @larsgrefer, who provided the answer in the GitHub Issue (46).

For the io.freefair.aspectj.compile-time-weaving plugin 2.9.5 the configuration is named "aspects" instead of "aspect".

The following fixed the issue:

aspects project(":aspectj:aspect")

The full build file resembles:

buildscript {
    repositories {
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        // 2.9.5 for use with Gradle 4.10.3.
        classpath "io.freefair.gradle:aspectj-plugin:2.9.5"
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'

apply plugin: "io.freefair.aspectj.compile-time-weaving"
aspectj.version = '1.9.3'

group 'xyz.swatt'
version '1.0.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {

    ///// SWATT ///// https://mvnrepository.com/artifact/xyz.swatt/swatt
    compile group: 'xyz.swatt', name: 'swatt', version: '1.12.0'
    aspect "xyz.swatt:swatt:1.12.0"
}

Question:

I'm using Gradle 4.5.1, gradle-aspectj 2.0 plugin and some others. The problem is that aspectj's task compileAspect is being executed before compileJava. Seems like I've found a problem here. So I've tried to reorder the tasks execution in the following way:

project.tasks['compileJava'].dependsOn.remove(project.tasks['compileAspect'])
project.tasks['compileAspect'].dependsOn(project.tasks['compileJava'])

I've also tried almost the same replacing project.tasks[taskName] with taskName. However, after reorderig compileAspect task is being ignored (completely, not skipped). What's more: although compileJava seems to be executed it doesn't produce any classes in the build/classes directory.

I am new to Gradle so maybe I'm doing a mistake somewhere in the reordering? Please, see the Github repository as an example producing the issue.

Thank you very much for any help!


Answer:

What's more: although compileJava seems to be executed it doesn't produce any classes in the build/classes directory.

This seems to be the intention of the Gradle AspectJ plugin. The line before the one you linked (62) contains:

project.tasks[javaTaskName].deleteAllActions()

This causes the compileJava task to do nothing at all and this is why, for the plugin, the order of the tasks does not matter. The plugin aims to completely replace compileJava task with its compileAspect task.

Please note, that the Gradle AspectJ plugin was archived four months ago:

We do not use this code any longer and this repository has been archived.

If you plan to use both compilation steps (Java and AspectJ) in one build, you may create your own plugin. Maybe you can fork the existing project and reuse the task class.

Question:

Hi I am building a library for messaging between AWS components and so I want a lightweight solution. One part of the solution requires me to listen in on when an annotated method is invoked, so I thought I'd use a pointcut and implement an advice on what to do when that pointcut was reached.

The gradle.build script looks like this:

buildscript {
    ext {
        // some company-specific config here
        nexus = {
            credentials {
                username nexusBuildUserToken
                password nexusBuildPassToken
            }
            url nexusRepoURL
        }
    }

    repositories {
        mavenCentral()
        maven(nexus)
    }


    dependencies {
        classpath("net.researchgate:gradle-release:$gradleReleasePluginVersion")
        classpath("gradle.plugin.aspectj:gradle-aspectj:$gradleAspectJPluginVersion")

    }
}

apply plugin: 'java'

// IDE
apply plugin: 'idea'
apply plugin: 'eclipse-wtp'

apply plugin: "aspectj.gradle"

jar {
    enabled = true
}

// project artifact info
group = groupId
archivesBaseName = artifactId

repositories {
    mavenCentral()
    maven(nexus)
    maven {
        url "https://dl.bintray.com/findify/maven"
    }

}

dependencies {

    compile("org.aspectj:aspectjtools:$aspectjVersion")

    compile("org.apache.commons:commons-lang3:${commonsLang3Version}")
    compile("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${jacksonDataformatYamlVersion}")

    compile("org.elasticmq:elasticmq-rest-sqs_2.11:0.14.1")
    compile("com.amazonaws:aws-java-sdk-sqs:${awsMessagingVersion}")
    compile("com.amazonaws:aws-java-sdk-sns:${awsMessagingVersion}")
    compile("au.com.auspost:json-encryption:${jsonEncryptionVersion}")

    compile("org.apache.commons:commons-lang3:${commonsLang3Version}")
    compile("org.reflections:reflections:${reflectionsVersion}")
    compile("redis.clients:jedis:${jedisVersion}")
    compile("org.aspectj:aspectjweaver:$aspectjVersion")
    compile("org.aspectj:aspectjrt:$aspectjVersion")

    testCompile("junit:junit:${jUnitVersion}")
    testCompile("org.mockito:mockito-core:${mockitoCoreVersion}")
    testCompile("org.assertj:assertj-core:${assertjVersion}")
    testCompile("ai.grakn:redis-mock:${embeddedRedisVersion}")
    testCompile("org.slf4j:slf4j-simple:1.7.25")
    testCompile("ch.qos.logback:logback-core:1.2.3")
    testCompile(group: 'io.findify', name: 'sqsmock_2.11', version: '0.3.2')
}

As you can see I included all the aspectj libs in an effort to make sure I'm not missing anything I need (feel free to tell me what I don't need).

The class I expect to get weaved is this:

@Aspect
public class TopicSenderManager {

    private ThreadPoolFactory threadPoolFactory;

    private CorrelationService correlationService = new CorrelationService.Default();

    private Map<String, TopicSenderProcessor> topicSenderProcessors = new HashMap<>();


    private ExecutorService executor;

    public TopicSenderManager(Map<String, TopicSenderProcessor> topicSenderProcessors) {
        this.threadPoolFactory = new ThreadPoolFactory.Default();
        this.topicSenderProcessors = topicSenderProcessors;
        executor = threadPoolFactory.create(topicSenderProcessors.size(), "TopicSender");
    }

    @Around("@annotation(topicSender) && execution(* *(..))")
    public Object sendMessageToTopic(ProceedingJoinPoint pjp, TopicSender topicSender) throws Throwable {

        TopicSenderProcessor topicSenderProcessor = getProcessor(topicSender.topicAlias(), topicSender.eventType());
        topicSenderProcessor.setCorrelationId(correlationService.read());
        Object[] args = pjp.getArgs();
        if (args == null || args.length != 1) {
            throw new Exception("naughty, naughty");
        } else {
            topicSenderProcessor.setEventMessage((EventMessage) args[0]);
            executor.execute(topicSenderProcessor);
        }    
        return pjp.proceed();
    }

    public Map<String, TopicSenderProcessor> getTopicSenderProcessors() {
        return topicSenderProcessors;
    }

    public TopicSenderProcessor getProcessor(String topic, String eventType) {
        String senderKey = topic + "," + eventType;
        return topicSenderProcessors.get(senderKey);
    }
}

What I'm hoping this will do is pick up every execution (invocation) of a method annotated with @TopicSender and execute the associated processor in a thread from the threadpool.

I had a look at the decompiled class of the TopicSenderManager which is shown below:

@Aspect
public class TopicSenderManager {
    private ThreadPoolFactory threadPoolFactory = new Default();
    private CorrelationService correlationService = new au.com.auspost.messaging.CorrelationService.Default();
    private Map<String, TopicSenderProcessor> topicSenderProcessors = new HashMap();
    private ExecutorService executor;

    public TopicSenderManager(Map<String, TopicSenderProcessor> topicSenderProcessors) {
        this.topicSenderProcessors = topicSenderProcessors;
        this.executor = this.threadPoolFactory.create(topicSenderProcessors.size(), "TopicSender");
    }

    @Around("@annotation(topicSender) && execution(* *(..))")
    public Object sendMessageToTopic(ProceedingJoinPoint pjp, TopicSender topicSender) throws Throwable {
        TopicSenderProcessor topicSenderProcessor = this.getProcessor(topicSender.topicAlias(), topicSender.eventType());
        topicSenderProcessor.setCorrelationId(ajc$inlineAccessFieldGet$au_com_auspost_messaging_send_topic_TopicSenderManager$au_com_auspost_messaging_send_topic_TopicSenderManager$correlationService(this).read());
        Object[] args = pjp.getArgs();
        if (args != null && args.length == 1) {
            topicSenderProcessor.setEventMessage((EventMessage)args[0]);
            ajc$inlineAccessFieldGet$au_com_auspost_messaging_send_topic_TopicSenderManager$au_com_auspost_messaging_send_topic_TopicSenderManager$executor(this).execute(topicSenderProcessor);
            return pjp.proceed();
        } else {
            throw new Exception("naughty, naughty");
        }
    }

    public Map<String, TopicSenderProcessor> getTopicSenderProcessors() {
        return this.topicSenderProcessors;
    }

    public TopicSenderProcessor getProcessor(String topic, String eventType) {
        String senderKey = topic + "," + eventType;
        return (TopicSenderProcessor)this.topicSenderProcessors.get(senderKey);
    }

    public static TopicSenderManager aspectOf() {
        if (ajc$perSingletonInstance == null) {
            throw new NoAspectBoundException("au.com.auspost.messaging.send.topic.TopicSenderManager", ajc$initFailureCause);
        } else {
            return ajc$perSingletonInstance;
        }
    }

    public static boolean hasAspect() {
        return ajc$perSingletonInstance != null;
    }

    static {
        try {
            ajc$postClinit();
        } catch (Throwable var1) {
            ajc$initFailureCause = var1;
        }

    }
}

I assume this is the weaving that is supposed to have happened.

The next thing is to set up a (sort of) unit test that demonstrates the behaviour that I want:

Firstly I define a simple example of the sort of method that I want to intercept:

public class SenderT1E1 {

    @TopicSender(topicAlias = "t1", eventType = "a.c.a.e1")
    public void aSendingMethod(EventMessage<TestMessage> eventMessage) {
        // do what you like before sending the message
    }
}

Then the test that looks at it:

public class SenderE1T1Test {

    static TopicSenderManager manager;

    @BeforeClass
    public static void setUp() throws Exception {

        MockAmazonClient snsClient = new MockAmazonClient();

        TopicSenderFactory senderFactory = new TopicSenderFactory();

        PublishResult publishResult = new PublishResult().withMessageId("E1T1");

        manager = senderFactory.make(snsClient.amazonSNS(publishResult), "aws-send-test.properties");
    }

    @Test
    public void whenSenderIsCalledMessageIsSent() {
        SenderT1E1 target = new SenderT1E1();

        EventMessage message = new EventMessage<>();
        message.setEventType("a.c.a.e1");
        message.setPayload(new TestMessage());

        target.aSendingMethod(message);

        TopicSenderProcessor processor = manager.getProcessor("http://localhost:8001/topic/t1", "a.c.a.e1");

        assertThat(manager.getTopicSenderProcessors().entrySet().size(), is(2));
        manager.getTopicSenderProcessors().forEach((k,v) -> System.out.println(k + ",  " + v));

        assertThat(processor, is(notNullValue()));

        // now, did it execute...
        assertThat(processor.getEventMessage(), is(notNullValue()));
        assertThat(processor.getLastPublishRequest(), is(notNullValue()));
        assertThat(processor.getLastPublishResult(), is(notNullValue()));
    }
}

Basically, everything up to the // now did it execute... line works, but everything is null after that. So it looks like the pointcut is never reached.

So, is the weaving not working correctly? is the pointcut and advice in the @Around spec not correct? is there some kind of runtime trigger, I have forgotten about? or is it something else?

I thought I should make a statement here: the reason I'm not using spring is that I have found that the number of dependencies leads to a really bloated library and I'm trying to keep it as trim as possible. I want the speed benefits of aspectJ and i would prefer compile time weaving because this is a library to be used in applications I will resort to spring if I have to, but it really is a last resort.


Answer:

The problem was in the something else category. The reason the Pointcut was not being triggered was because I was using compile-time weaving, and the weaved target class was not in the classpath at runtime. The weaved target class was in src/test/java, while the pointcut was in src/main/java and when the application is compiled, src/test/java is no where in sight. What I needed was runtime weaving, so that the test class could be discovered and weaved, and then the pointcut would work. Unfortunately, runtime weaving is an expensive operation, so not an option for my purposes, so I have gone over to using listener pattern. Its not as clean from an implementors perspective, but much better performance-wise