17

i want to use aspectj aop in kotlin,here is my code:

my annotation in annotation.lazy_list:

Kotlin:

 package anotation

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class lazy_list

my aspectj aop class:

@Aspect
class ActiveListAop{

    @Pointcut("execution(@annotation.lazy_list * *(..))")
    fun profile() {

    }

    @Before("profile()")
    fun testModeOnly(joinPoint: JoinPoint) {
        println("123")
    }

}

my usage:

 @lazy_list
    fun all():List<T>{
        return lazy_obj?.all() as List<T>
    }

when i call all() function , no error,but wont't print "123", why?

junk
  • 917
  • 3
  • 11
  • 29

5 Answers5

7

EDIT 9-2021 - there is a nice updated plugin for android that works well as an alternate to my original 2018 answer below: https://github.com/Ibotta/gradle-aspectj-pipeline-plugin

For what it's worth, we needed aspectJ weaving in our android project but really wanted to move to kotlin so we had to solve this problem. So the solutions in this thread using spring or maven didn't work for us. This is the solution for android gradle projects however, this WILL break incremental compilation and therefor slow down your build times and/or break something eventually. This gets us by until I can re-think our architecture and phase out aspectJ or (hopefully) android starts supporting it.

There is confusion in some of the answers and comments to the OP that kapt solves this, but kapt lets you do compile time annotation processing, not weaving. That is, annotation processors let you generate code based on annotations but do not let you inject logic into existing code.

This builds on top of this blog on adding aspectJ to android: https://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android

Your kotlin classes get compiled into byte code, just into a different directory. So this solution using the same process to weave the java classes but runs it again on the kotlin class files

at the top of your App/build.gradle add:

buildscript {
    ext.aspectjVersion = '1.9.1'
    dependencies {
        classpath "org.aspectj:aspectjtools:$aspectjVersion"
    }
}

At the bottom of your App/build.gradle add:

android.applicationVariants.all { variant ->

// add the versionName & versionCode to the apk file name
variant.outputs.all { output ->
    def newPath = outputFileName.replace(".apk", "-${variant.versionName}.${variant.versionCode}.apk")
    outputFileName = new File(outputFileName, newPath)


    def fullName = ""
    output.name.tokenize('-').eachWithIndex { token, index ->
        fullName = fullName + (index == 0 ? token : token.capitalize())
    }

    JavaCompile javaCompile = variant.javaCompiler

    MessageHandler handler = new MessageHandler(true)
    javaCompile.doLast {
        String[] javaArgs = ["-showWeaveInfo",
                             "-1.8",
                             "-inpath", javaCompile.destinationDir.toString(),
                             "-aspectpath", javaCompile.classpath.asPath,
                             "-d", javaCompile.destinationDir.toString(),
                             "-classpath", javaCompile.classpath.asPath,
                             "-bootclasspath", project.android.bootClasspath.join(
                File.pathSeparator)]

        String[] kotlinArgs = ["-showWeaveInfo",
                               "-1.8",
                               "-inpath", project.buildDir.path + "/tmp/kotlin-classes/" + fullName,
                               "-aspectpath", javaCompile.classpath.asPath,
                               "-d", project.buildDir.path + "/tmp/kotlin-classes/" + fullName,
                               "-classpath", javaCompile.classpath.asPath,
                               "-bootclasspath", project.android.bootClasspath.join(
                File.pathSeparator)]

        new Main().run(javaArgs, handler)
        new Main().run(kotlinArgs, 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:
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break
            }
        }
    }
}
bsautner
  • 4,479
  • 1
  • 36
  • 50
  • My kotlin classes are not being weaved even after adding this code. Any guesses why? Though java classes are working perfectly fine, and getting weaved in the same project.(I have both kotlin and java classes in a project.) – Mohd Naved Sep 16 '19 at 05:14
  • 1
    I'd suggest doing a reality check on the build path in the bottom of the script and verify the kotlin class files are in that directory. Gradle changes the path names for build outputs in gradle 5 so it could be different – bsautner Sep 16 '19 at 14:12
4

spring + kotlin + AOP work nice, just go to http://start.spring.io/ and generate a project with AOP support, you can see a piece of build.gradle here...

buildscript {

    ext {
        kotlinVersion = '1.2.30'
        springBootVersion = '2.0.0.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
    }
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'org.springframework.boot'

...

dependencies {
    compile('org.springframework.boot:spring-boot-starter-aop')
    ...
}

plugin kotlin-spring makes all classes open to allow AOP

Then, just declare your aspect as follows

@Aspect
@Component
class MyAspect {
...

Important: annotate your aspect class with @Aspect and @Component annotations

Piece of cake! :)

  • This only works for Spring managed beans, not for compile time weaving which I believe would be required to intercept interaction with a `List` class – jmlw Oct 24 '18 at 15:59
  • 2
    The question is about **AspectJ** not Spring-AOP. In addition to only working for managed beans, Spring-AOP has other limitations if compared with AspectJ. For example, it does not support compile time error and warning declarations. – Paulo Merson Jun 07 '19 at 19:52
3

For annotation process in Kotlin, you must enable and use KAPT. Without this being added via Gradle or Maven plugin, nothing is going to work for annotation processing in Kotlin code.

The Kotlin plugin supports annotation processors like Dagger or DBFlow. In order for them to work with Kotlin classes, apply the kotlin-kapt plugin.

See also:

Jayson Minard
  • 84,842
  • 38
  • 184
  • 227
  • @junk - Is a pure AspectJ app or a Spring app? It could be something related to final by default classes – Mario Arias Jun 05 '17 at 15:04
  • @MarioArias it is a pure Aspectj + Kotlin app,no Spring. no any java code – junk Jun 05 '17 at 15:09
  • See my answer [here](https://stackoverflow.com/questions/44364633/aspectj-doesnt-work-with-kotlin) for a complete working example. – Robby Cornelissen Aug 30 '18 at 08:47
  • @RobbyCornelissen The link you provided is wrong(which points to this question). I found your another answer that works, which might be what you actually meant: https://stackoverflow.com/questions/52087239/kotlin-possible-to-modify-functions-during-compile-time-through-metaprogramming/52092531#52092531 – Jacob Wu Oct 06 '18 at 14:21
3

You can use freefair gradle plugin

buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "io.freefair.gradle:aspectj-plugin:5.2.1"
  }
}

apply plugin: "io.freefair.aspectj.post-compile-weaving"
almayce
  • 33
  • 4
0

So I think I've got a good (but wordy) solution for Android. At the time of writing I'm using Gradle 6.7, Android plugin 4.1.0, and AspectJ tools 1.9.6.

The gist of the problem is that:

  • Java is compiled by task compileDebugJavaWithJavac
  • Kotlin is compiled by task compileDebugKotlin
  • Gradle can run either one of these tasks, or both of them, or none
  • compileDebugJavaWithJavac depends on compileDebugKotlin
  • weaving Kotlin usually requires Java classes.

If you look at these points closely, you'll see that you can't do weaving as part of compiling Kotlin, as Java classes can be missing at this point. If you do that, you'll get warnings such as:

WARN: incorrect classpath: C:\Users\user\StudioProjects\myapp\app\build\intermediates\javac\debug\classes

and errors such as

ERROR: can’t determine modifiers of missing type myapp.Foo.Bar

So the better approach would be to postpone weaving until Java classes are compiled. But as you would be modifying files not as a part of compilation task, you lose incremental builds... Besides, this postponed weaving is super hard to get right—remember, none of the compile tasks might be actually scheduled for running!


The real solution is to wrap weaving in a Transform, which will produce a Gradle task with its own inputs and outputs. This means that you will not be polluting the files of compile tasks, and those tasks, as well as this task, will be, so to say, UP-TO-DATE-able. This requires quite a bit of code, but it's rather sensible!

First, put this in your project build.gradle.kts:

buildscript {
    dependencies {
        classpath("org.aspectj:aspectjtools:1.9.6")
    }
}

This is needed to run weaving from inside “inside” the buildscript. If you want to run weaving in a separate process, which is a good idea on Windows, you will need the path of this jar, which you can get by adding the following to your app build.gradle.kts:

val weaving: Configuration by configurations.creating

dependencies {
    weaving("org.aspectj:aspectjtools:1.9.6")
}

Finally, put AspectJ runtime on the classpath (app build.gradle.kts, note that I only need weaving in debug builds):

dependencies {
    debugImplementation("org.aspectj:aspectjrt:1.9.6")
}

Now, here's my setup. I have a local logging library, :cats, which containts aspects that I want to weave. Logging statements are only inside my project, and not anywhere else. Also, I only want to run these in debug builds. So here's the transformation that “weaves cats” into the app (app's build.gradle.kts):

class TransformCats : Transform() {
    override fun getName(): String = TransformCats::class.simpleName!!

    override fun getInputTypes() = setOf(QualifiedContent.DefaultContentType.CLASSES)

    // only look for annotations in app classes
    // transformation will consume these and put woven classes in the output dir
    override fun getScopes() = mutableSetOf(QualifiedContent.Scope.PROJECT)

    // ...but also have the rest on our class path
    // these will not be touched by the transformation
    override fun getReferencedScopes() = mutableSetOf(QualifiedContent.Scope.SUB_PROJECTS,
                                                      QualifiedContent.Scope.EXTERNAL_LIBRARIES)

    override fun isIncremental() = false

    // only run on debug builds
    override fun applyToVariant(variant: VariantInfo) = variant.isDebuggable

    override fun transform(invocation: TransformInvocation) {
        if (!invocation.isIncremental) {
            invocation.outputProvider.deleteAll()
        }

        val output = invocation.outputProvider.getContentLocation(name, outputTypes,
                                                                  scopes, Format.DIRECTORY)
        if (output.isDirectory) FileUtils.deleteDirectoryContents(output)
        FileUtils.mkdirs(output)

        val input = mutableListOf<File>()
        val classPath = mutableListOf<File>()
        val aspectPath = mutableListOf<File>()

        invocation.inputs.forEach { source ->
            source.directoryInputs.forEach { dir ->
                input.add(dir.file)
                classPath.add(dir.file)
            }

            source.jarInputs.forEach { jar ->
                input.add(jar.file)
                classPath.add(jar.file)
            }
        }

        invocation.referencedInputs.forEach { source ->
            source.directoryInputs.forEach { dir ->
                classPath.add(dir.file)
            }

            source.jarInputs.forEach { jar ->
                classPath.add(jar.file)
                if (jar.name == ":cats") aspectPath.add(jar.file)
            }

        }

        weave(classPath, aspectPath, input, output)
    }
}

android.registerTransform(TransformCats())

And here's the weaving code mentioned above:

// ajc gets hold of some files such as R.jar, and on Windows it leads to errors such as:
//   The process cannot access the file because it is being used by another process
// to avoid these, weave in a process, which `javaexec` will helpfully launch for us.

fun weave(classPath: Iterable<File>, aspectPath: Iterable<File>, input: Iterable<File>, output: File) {
    val runInAProcess = OperatingSystem.current().isWindows
    val bootClassPath = android.bootClasspath

    println(if (runInAProcess) ":: weaving in a process..." else ":: weaving...")
    println(":: boot class path:  $bootClassPath")
    println(":: class path:       $classPath")
    println(":: aspect path:      $aspectPath")
    println(":: input:            $input")
    println(":: output:           $output")

    val arguments = listOf("-showWeaveInfo",
                           "-1.8",
                           "-bootclasspath", bootClassPath.asArgument,
                           "-classpath", classPath.asArgument,
                           "-aspectpath", aspectPath.asArgument,
                           "-inpath", input.asArgument,
                           "-d", output.absolutePath)

    if (runInAProcess) {
        javaexec {
            classpath = weaving
            main = "org.aspectj.tools.ajc.Main"
            args = arguments
        }
    } else {
        val handler = MessageHandler(true)
        Main().run(arguments.toTypedArray(), handler)

        val log = project.logger
        for (message in handler.getMessages(null, true)) {
            when (message.kind) {
                IMessage.DEBUG -> log.debug("DEBUG " + message.message, message.thrown)
                IMessage.INFO -> log.info("INFO: " + message.message, message.thrown)
                IMessage.WARNING -> log.warn("WARN: " + message.message, message.thrown)
                IMessage.FAIL,
                IMessage.ERROR,
                IMessage.ABORT -> log.error("ERROR: " + message.message, message.thrown)
            }
        }
    }
}

val Iterable<File>.asArgument get() = joinToString(File.pathSeparator)

(The Windows part is using weaving configuration; you may not want either part of the if)

This is it!

Edit: As of AGP 4.2.0, jar.name doesn't return anything useful. For the time being, I used this fragile workaround:

if (jar.file.directoriesInsideRootProject().contains("cats")) {
    aspectPath.add(jar.file)
}

fun File.directoriesInsideRootProject() = sequence {
    var file = this@directoriesInsideRootProject
    while (true) {
        yield(file.name)
        file = file.parentFile ?: break
        if (file == rootProject.projectDir) break
    }
}
squirrel
  • 5,114
  • 4
  • 31
  • 43