I have a package in my app and I'd like to go over all classes in that package to then generate some JSON schema automatically.
I'd like to create a gradle task and with some sort of build-time dependency would allow me to do:
tasks.register("my fancy task") {
doLast {
"my.package.name".readKotlinFiles().classes.forEach { klass ->
klass.properties["id"]... and do something here
}
}
}
How can you do such thing easily?
I was doing something similar, loading classes form the project, but not as a Gradle task or plugin but in Java application itself. To execute this logic as Gradle task / plugin I would do it in the following way:
First you generate jar package from your Kotlin project as a standard process of Kotlin-Gradle build.
As Gradle build is not a part of your project source code you have to load classes of the actual project with properly configured ClassLoader.
Then you can read those classes and generate the report.
Now that would translate into the pseudocode of a Gradle task:
tasks.register("processClasses") {
dependsOn tasks.named("assemble") // generate jar
doLast {
// Initiate classloader of assembled project
// Read target package from gradle properties or other kind of parameters
// Get list of all classes under the specific package
// Load the list of classes from classloader
// Execute custom logic upon the list of classes
// Generate report in specific format (JSON, YAML, HTML, ...)
}
}
I tried to validate the logic and created a POC as the following GitHub project. There are two flavors of build scripts:
Groovy build script is in master branch
Kotlin build script version is in kotlin branch. Note multiple
aspects should be improved and adapted to the specific goal you have
in mind (really curious actually). You are welcome to make any
suggestions, fork the code, do changes..
Excerpt of the code below:
// Pack all the dependencies into jar
tasks.jar {
from(configurations.runtimeClasspath.map { configuration ->
configuration.asFileTree.fold(files().asFileTree) { collection, file ->
if (file.isDirectory) collection else collection.plus(zipTree(file))
}
})
}
val packageToProcess: String by project
tasks.register("processClasses") {
group = "process"
description = "Process classes form a specific package"
dependsOn(tasks.named("assemble"))
doLast {
// Instantiate classloader from jar as you will need other dependencies for loading classes
val file: File = project.projectDir.toPath().resolve(tasks.jar.get().archiveFile.get().toString()).toFile()
println("Packaged Kotlin project jar file: $file")
val classloader: ClassLoader = URLClassLoader(arrayOf(file.toURI().toURL()))
// Iterate through all of the class files in the specified package
val packageToScan: String = packageToProcess.replace(".", File.separator)
val path: java.nio.file.Path = project.file("build/classes/kotlin/main/").toPath().resolve(packageToScan)
val classesFromSpecificPackage = Files.walk(path)
.filter {
Files.isRegularFile(it)
}
.map {
project.file("build/classes/kotlin/main/").toPath().relativize(it)
}
.map { it.toString().substring(0, it.toString().lastIndexOf(".")).replace(File.separator, ".") }
.map {
//println("Class = $it") // print class if necessary before loading it
classloader.loadClass(it)
}
// Do something with the classes form specific package
// Now just basic information is printed directly to the console
println("=======================================")
println("Classes from package: $packageToScan")
println("")
classesFromSpecificPackage.forEach {
println(" Class: ${it.canonicalName}")
val fields = it.declaredFields
for (element in fields) {
println(" - Declared field: $element")
}
println("")
}
println("=======================================")
}
}
I could use a helping hand converting victor trellos simple sourceSet extension to Kotlin DSL, I seem a bit lost.
Victor Trello has an extension on SourceSet named svg
project.extensions.create('victor', VictorPluginExtension)
// Add 'svg' as a source set extension
project.android.sourceSets.all { sourceSet ->
SourceDirectorySet sds = project.objects.sourceDirectorySet(sourceSet.name, "${sourceSet.name} svgs")
sourceSet.extensions.add('svg', sds)
}
This is how it is configured in Groovy:
// build.gradle:
android {
// Variant 1 with individual setup
sourceSets {
main {
svg.srcDir 'src/main/svg'
}
}
// Variant 2 handling all at once
sourceSets.all { sourceSet ->
svg.srcDir "src/${sourceSet.name}/svg"
}
}
// Plugin configuration of class com.trello.victor.VictorPluginExtension
victor {
svgDpi = 72
generateVectorDrawables = true
}
What would this look like in Kotlin DSL?
Here's the Victor Trello Gradle Plugin source
I notice that the Victor plugin extends SourceSet, and project.sourceSet has type SourceSet (from gradle) which contains an extensions, whereas AndroidSourceSet does not contain the .extensions. Also I can seem to get a gradle SourceSet from the AndroidSourceSet
This might be the right way to do the plugin configuration, though I cannot test it without the srcDir (I'll remove this from the question if its wrong, to avoid confusion for future readers).
// build.gradle.kts:
configure<com.trello.victor.VictorPluginExtension> {
svgDpi = 72
generateVectorDrawables = true
}
I found the solution with the help of several people (Google + Gradle forums). Hopefully this can help others with other plugins.
The solution had three parts to solve,
Configure a plugin with the configure()
Access Groovy extensions
Iterate sourceSets.all in kotlin (which is obvious if your not unlucky enough to start with sourceSets.all { sourceSet -> })
Here's the source
val Any.extensions get() = (this as org.gradle.api.plugins.ExtensionAware).extensions
android {
sourceSets {
// Variant 1 with individual setup
named("main") {
val a: com.android.build.api.dsl.AndroidSourceSet = this
java.srcDir("src/sharedTest/java")
}
}
// Variant 2 handling all at once
sourceSets.all {
val svgSourceSet = this.extensions["svg"] as SourceDirectorySet
svgSourceSet.srcDir("src/${name}/svg")
}
}
configure<com.trello.victor.VictorPluginExtension> {
// Any assets defined in relative terms needs a base DPI specified
svgDpi = 72
// Do not generate these densities for SVG assets
excludeDensities = listOf("ldpi", "xxxhdpi")
// Set this to "true" if you want to generate Android vectors instead of PNGs
generateVectorDrawables = false
}
According to this from the official docs and this SO answer, defining a new source set should be as simple as (in kotlin):
sourceSets {
create("demo") {
compileClasspath += sourceSets.main.get().output
}
}
The second reference also explicitly claims this can now be used in building a jar. However, while the above does not throw an error, actually trying to use the new source set anywhere does. Minimal example (generated with gradle init -> "kotlin application" and project name "foobar"):
plugins {
id("org.jetbrains.kotlin.jvm").version("1.3.21")
application
}
repositories {
jcenter()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
}
application {
mainClassName = "foobar.AppKt"
}
sourceSets {
create("demo") {
compileClasspath += sourceSets.main.get().output
}
}
I've removed configuration of the test source set since it isn't relevant here. The src directory looks like this (which is a bit non standard, but I think fine for the discussion):
src
├── demo
│ └── Demo.kt
└── main
└── foobar
└── App.kt
Ideally, I'd like to have completely separate source trees (but this question is not specifically about that). What this builds makes sense in relation to the configuration above, but it is not really the goal. To do that, I want to add a custom jar or two.1
Unfortunately though, the demo source set can't be referenced in the gradle script.
val jar by tasks.getting(Jar::class) {
manifest { attributes["Main-Class"] = "DemoKt" }
// HERE IS THE PROBLEM:
from(sourceSets["demo"].get().output)
dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
This fat jar task definition is taken from the gradle docs and I've used much before.
As is, gradle chokes:
Script compilation errors:
Line 28: from(sourceSets["demo"].get().output)
^ Unresolved reference.
If I change that line to:
from(sourceSets.demo.get().output)
Which is the syntax used with sourceSets.main, then the error becomes:
Line 28: from(sourceSets.demo.get().output)
^ Unresolved reference: demo
How am I supposed to work with the custom source set?
I am aware of the gradle subproject pattern and have used it to do much the same thing, but I'd prefer the simple separate source sets solution, which does I think meet all three criteria for such mentioned in that doc.
The sourceSets property (an extension property for Project) returns a SourceSetContainer which extends NamedDomainObjectContainer<SourceSet>. The latter interface defines get(String) (an operator extension function) which is what allows you to use the ["..."] syntax. That method returns T directly, meaning you should just be using:
from(sourceSets["demo"].output)
The reason you have to use get() when using sourceSets.main is because you're getting a NamedDomainObjectProvider<SourceSet> which wraps the source set.
You can also get the custom source set via delegation
When creating it:
val demo by sourceSets.creating { /* configure */ }
Or some time after creating it:
val demo by sourceSets
I've written a vertx service interface in Kotlin, for which I am trying to generate service proxies. However, apart from generating the generated directory in src/main, it does nothing.
src/main/java/amb85/portfolio/package-info.java:
#ModuleGen(name = "portfolio", groupPackage = "amb85.portfolio")
package amb85.portfolio;
import io.vertx.codegen.annotations.ModuleGen;
I then have the following service interface src/main/kotlin/amb85/portfolio/PortfolioService.kt:
#VertxGen
#ProxyGen
interface PortfolioService {
companion object {
val ADDRESS = "service.portfolio"
val EVENT_ADDRESS = "portfolio"
}
fun getPortfolio(resultHandler: (AsyncResult<Portfolio>) -> Unit)
fun buy(amount: Int, quote: JsonObject, resultHandler: (AsyncResult<Portfolio>) -> Unit)
fun sell(amount: Int, quote:JsonObject, resultHandler: (AsyncResult<Portfolio>) -> Unit)
fun evaluate(resultHandler: (AsyncResult<Double>) -> Unit)
}
And the relevant configuration from build.gradle:
task generateProxies(type: JavaCompile, group: "build",
description: "Generates the Vert.x proxies") { // codegen
source = sourceSets.main.java
source += sourceSets.main.kotlin
classpath = configurations.compile + configurations.compileOnly
destinationDir = project.file("${projectDir}/src/main/generated")
options.compilerArgs = [
"-proc:only",
"-processor", "io.vertx.codegen.CodeGenProcessor",
"-Acodegen.output=${project.projectDir}/src/main"
]
}
I then run ./gradlew portfolio:generateProxies, but nothing beyond the generated directory.
Is it possible to use vertx-codegen to generate service proxies based on an interface written in Kotlin? If so, what configuration steps am I missing? If not, is there any other way to generate the proxies? Even better, is there a way to do it entirely in Kotlin, avoiding the java generation or using it as an intermediate step?
The easiest way to use vertx service proxies with kotlin is to use kapt and vertx-codegen processor classified dependency.
In your build.gradle you should add following:
apply plugin: 'kotlin-kapt'
dependencies {
kapt "io.vertx:vertx-codegen:$vertx_version:processor"
compileOnly "io.vertx:vertx-codegen:$vertx_version"
// other deps go here
}
Nothing else needed so far.
Add the service proxy procesor in order to generate the proxy.
kapt "io.vertx:vertx-codegen:$vertxVersion:processor"
kapt "io.vertx:vertx-service-proxy:$vertxVersion:processor"
compile "io.vertx:vertx-service-proxy:$vertxVersion"
And if you are going to generate the service with #VertxGen you should use the package-info.java even if you are writing Kotlin:
#ModuleGen(name = "example", groupPackage = "com.some")
package com.something;
import io.vertx.codegen.annotations.ModuleGen;
You should probably define a source set for generated. I have a separate Gradle script gradle/vertx-codegen.gradle that I include where needed, and that works fine (its only for Java, so you should adapt it a bit):
sourceSets {
generated{
java.srcDir "${projectDir}/src/generated/java"
}
}
task generateProxies(type: JavaCompile) {
group = "build"
description = "Generate Vert.x service proxies"
source = sourceSets.main.java
classpath = configurations.compile
options.compilerArgs = [
"-proc:only",
"-processor", "io.vertx.codegen.CodeGenProcessor",
"-AoutputDirectory=${projectDir}/src/main"
]
destinationDir = file("${projectDir}/src/generated/java")
}
compileJava{
dependsOn generateProxies
source += sourceSets.generated.java
}
clean {
delete += sourceSets.generated.java.srcDirs
}
Hope this helps!
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?
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
}
}
}
}
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! :)
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:
Pushing the limits of Kotlin annotation processing
kapt: Annotation Processing for Kotlin
Better Annotation Processing: Supporting Stubs in kapt
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"
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
}
}