11

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

Flo
  • 1,469
  • 1
  • 18
  • 27
  • 1
    I would like to help with the AspectJ compiler part, but have never used Gradle, only Maven. Can you make the problem reproduceable by publishing a minimal, fully self-consistent version of your project (just with a few dummy classes maybe) on GitHub? – kriegaex Nov 16 '14 at 12:41
  • Thanks! I updated the question, please check it out. – Flo Nov 17 '14 at 13:40
  • Sorry, I cannot build this. It seems that I need to install Android ADK incl SDK and I think I have even though it took a long time. But somehow it is not found or is the wrong version. Usually a Maven build gets *all* of its dependencies properly resolved and downloaded, is this different in Gradle? Can you somehow create reproduceable build for me without all the Android stuff, just concentrating on plain Java + AspectJ? – kriegaex Nov 17 '14 at 14:51
  • Okay, with a lot of trial & error (`ANDROID_BUILD_TARGET_SDK_VERSION` was wrong for my installation) I was able to compile the project. There also was an NPE because in the *app* subproject I had to change an *ajc* parameter to `"-log", "weave.log",` because your local path did not exist on my machine. Now do do I run the project locally in order to reproduce the problem? – kriegaex Nov 17 '14 at 16:07
  • I updated the repo and took out that local logging path of ajc. You can just open the /build.gradle with Android studio or you can use `gradle clean installDebug` to install on your device/emulator. – Flo Nov 18 '14 at 00:18
  • I do not have a device and need to find out how to install an emulator first. I am an Android newbie. Is it really not reproduceable without Android? Maybe I will try tomorrow. – kriegaex Nov 18 '14 at 00:28
  • I installed Android SDK 21 and created a Nexus 5 emulation (is another one better, I hav no idea?). The device boots up, I can deploy the app, but whenever I run it I just see "Unfortunately, com.test.sample has stopped." – kriegaex Nov 18 '14 at 14:09
  • Sorry to make you go through all this trouble, but I need to get this working on Android after all. I wouldn't know how to configure gradle to run with `ajc` with a java plugin anyway, and even if I did, it wouldn't necessarily solve the problem I am facing. – Flo Nov 18 '14 at 15:13
  • The error you are experiencing is exactly the problem my question describes. If you look at the logcat output (which you will see if you launch AndroidStudio and have a device connected), you should see the stacktrace, the cause of the exception should be `java.lang.NoSuchMethodError`. As I described, if you move the `TraceAspect.java` from the `annotation` module to the `app` module, it magically starts to work. – Flo Nov 18 '14 at 15:15
  • 1
    I can reproduce the problem now, but I am a Gradle noob in combination with an Android noob. What I can say in accordance with Andy is that you compile your aspect with a plain Java compiler, i.e. it is not "finished" yet. Then somehow your build process gets the unfinished aspect from *annotation.jar* into the APK at the end. I recommend to just build your annotation subproject with *Ajc* as well, then you should be fine. The alternative is to make sure that the finished rather than the unfinished aspect class goes into the APK. For that, get *annotation.jar* on the *inpath*. – kriegaex Nov 18 '14 at 23:29

3 Answers3

6

I run into the same problem but I used Maven instead of Gradle.

Before an aspect class can be applied to a target class it first needs to be 'weaved' into an aspect. A weaved aspect class will have two static methods added (aspectOf and hasAspect).

In my particular case I didn't weave my aspects.

It can be done by adding aspectj-maven-plugin to the build section.

<build>
<plugins>
  <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <executions>
      <execution>
        <goals>
          <goal>compile</goal> 
        </goals>
      </execution>
    </executions>
  </plugin>
</plugins>

Hope it helps!

Eugene Maysyuk
  • 2,977
  • 25
  • 24
5

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.

Andy Clement
  • 2,510
  • 16
  • 11
  • I believe I am executing `ajc` correctly in my gradle build file (via `new Main().run(args, handler)`). Adding `-log file` output to `ajc` shows that files are correctly weaved. Also, my code was not weaved, then the `@TraceDebug` annotation would just be a simple method annotation and the program would simply ignore it during runtime. But in my case the program crashes each time I attempt to execute an annotated method. – Flo Nov 11 '14 at 10:45
3

I played around with a Gradle AspectJ plugin and applied it to the annotation subproject like this:

buildscript {
    repositories {
        maven {
            url "https://maven.eveoh.nl/content/repositories/releases"
        }
    }

    dependencies {
        classpath "nl.eveoh:gradle-aspectj:1.4"
    }
}

project.ext {
    aspectjVersion = '1.8.4'
}

apply plugin: 'aspectj'

project.convention.plugins.java.sourceCompatibility = org.gradle.api.JavaVersion.VERSION_1_7
project.convention.plugins.java.targetCompatibility = org.gradle.api.JavaVersion.VERSION_1_7

Now the app works in the emulator and DDMS from the Android SDK shows that the advice output is on the console as expected. :-)

Debug Monitor console log

Please note that I have upgraded the project to AspectJ 1.8.4 and Java 7. I have also changed these settings:

Index: app/build.gradle
===================================================================
--- app/build.gradle    (revision 9d9c3ce4e0f903b5e7c650f231577c20585e6923)
+++ app/build.gradle    (revision )
@@ -2,8 +2,7 @@

 dependencies {
     // aspectJ compiler
-    compile 'org.aspectj:aspectjrt:1.8.1'
-
+    compile 'org.aspectj:aspectjrt:1.8.4'
     compile (project (':annotation'))
 }

@@ -50,13 +49,13 @@
     JavaCompile javaCompile = variant.javaCompile
     javaCompile.doLast {
         String[] args = ["-showWeaveInfo",
-                         "-1.5",
+                         "-1.7",
                          "-XnoInline",
                          "-inpath", javaCompile.destinationDir.toString(),
                          "-aspectpath", javaCompile.classpath.asPath,
                          "-d", javaCompile.destinationDir.toString(),
                          "-classpath", javaCompile.classpath.asPath,
-                         //"-log", "/home/flo/workspace/test-aspectj/weave.log",
+                         "-log", "weave.log",
                          "-bootclasspath", plugin.project.android.bootClasspath.join(File.pathSeparator)]

         MessageHandler handler = new MessageHandler(true);
Index: build.gradle
===================================================================
--- build.gradle    (revision 9d9c3ce4e0f903b5e7c650f231577c20585e6923)
+++ build.gradle    (revision )
@@ -5,7 +5,7 @@
     dependencies {
         classpath 'com.android.tools.build:gradle:0.12.+'
         // aspectj - http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
-        classpath 'org.aspectj:aspectjtools:1.8.1'
+        classpath 'org.aspectj:aspectjtools:1.8.4'
     }
 }
kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • Wow - I can confirm that it works, amazing! Thanks so much for all the investigation, I am really grateful that you dig into so many new technologies in order to get to the bottom of this. By any chance, would you also happen to know, why my previous script would fail and why it would work with your modifications? – Flo Nov 22 '14 at 03:17
  • I already explained a while ago in my last comment under your original question and Andy's answer explains what you have done: You have compiled the aspect with a normal `javac`, producing an unfinished aspect without `aspectOf()` method (check the byte code produced by your old variant for yourself). Then you put the aspect *aspectpath* (but not on the *inpath*!) of your parent module, i.e. the aspect was found for compilation, but *not* finished by the AspectJ compiler. Thus, you ended up with an unfinished aspect in your APK. My solution compiles the aspect with *ajc* to begin with. – kriegaex Nov 22 '14 at 10:25