xmolecules/jmolecules-examples

jmolecules-kotlin with build.gradle.kts not working

hanrw opened this issue · 8 comments

hanrw commented

Just want using gradle kotlin to run the example but not working
error logs

2022-07-16 23:05:52.505  INFO 10203 --- [           main] com.snacks.ApplicationKt                 : Started ApplicationKt in 2.37 seconds (JVM running for 2.533)
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.snacks.OrderRepository' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:351)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1172)
	at com.snacks.ApplicationKt.main(Application.kt:17)

build.gradle.kts configs below

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

buildscript {
    dependencies {
        classpath(platform("org.jmolecules:jmolecules-bom:2022.2.0"))
        classpath("org.jmolecules.integrations:jmolecules-bytebuddy")
        classpath("org.jmolecules.integrations:jmolecules-spring")
        classpath("org.jmolecules.integrations:jmolecules-jpa")
    }
}

plugins {
    id("org.springframework.boot") version "2.7.0"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.6.21"
    id("org.jetbrains.kotlinx.kover") version "0.5.1"
    kotlin("plugin.spring") version "1.6.21"
    kotlin("plugin.jpa") version "1.6.21"
    id("net.bytebuddy.byte-buddy-gradle-plugin") version "1.12.12"
    idea
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_17

repositories {
    mavenCentral()
}

dependencies {
    implementation(platform("org.jmolecules:jmolecules-bom:2022.2.0"))
    implementation("net.bytebuddy:byte-buddy-gradle-plugin:1.12.10")
    implementation("org.jmolecules.integrations:jmolecules-bytebuddy")
    implementation("org.jmolecules.integrations:jmolecules-spring")
    implementation("org.jmolecules.integrations:jmolecules-jpa")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.springframework.session:spring-session-jdbc")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jmolecules:kmolecules-ddd")

    implementation("org.springdoc:springdoc-openapi-ui:1.6.9")
    implementation("org.springdoc:springdoc-openapi-kotlin:1.6.9")
    implementation("com.vladmihalcea:hibernate-types-55:2.16.2")
    implementation("org.projectlombok:lombok")

    runtimeOnly("mysql:mysql-connector-java:8.0.29")
    implementation("com.h2database:h2:2.1.212")

    testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "17"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks {

    byteBuddy {
        transformation {
            plugin = org.jmolecules.bytebuddy.JMoleculesPlugin::class.java
        }
    }

    compileKotlin {
        kotlinOptions {
            freeCompilerArgs = listOf("-Xjsr305=strict")
            jvmTarget = "17"
        }
    }

    test {
        extensions.configure(kotlinx.kover.api.KoverTaskExtension::class) {
            isEnabled = true
            binaryReportFile.set(file("$buildDir/kover/test.exec"))
        }
        useJUnitPlatform()
//            testLogging {
//                events("passed", "skipped", "failed")
//            }
        finalizedBy(koverHtmlReport)
    }

    koverHtmlReport {
        isEnabled = true
        htmlReportDir.set(layout.buildDirectory.dir("reports/kover/html"))
    }
}

kotlin application

@SpringBootApplication
open class Application

fun main(args: Array<String>) {
    val context = runApplication<Application>(*args)

    val repository = context.getBean(OrderRepository::class.java)
    val order = repository.save(Order())
}

@Table(name = "KotlinOrder")
open class Order : AggregateRoot<Order, Order.OrderIdentifier> {

    override val id = OrderIdentifier(UUID.randomUUID())

    data class OrderIdentifier(val id: UUID) : Identifier
}

interface OrderRepository : CrudRepository<Order, Order.OrderIdentifier>
hanrw commented

and reproducer project - issue-6.zip

Looks like you already got the root cause of this here. Would've been nice if you had left a clue here as I just spent an hour investigating the issue, summarizing my findings, just to discover you already filed a ticket with ByteBuddy. 😕

hanrw commented

And tried to create a new plugin it works, but not working well - code will not generate corectlly. seems still some issue with it.

import net.bytebuddy.ClassFileVersion
import net.bytebuddy.build.EntryPoint
import net.bytebuddy.build.gradle.ByteBuddyTask
import net.bytebuddy.build.gradle.Discovery
import net.bytebuddy.build.gradle.IncrementalResolver
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.Directory
import org.gradle.api.tasks.compile.AbstractCompile
import org.jmolecules.bytebuddy.JMoleculesPlugin

class ByteBuddyKotlinPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println ("ByteBuddyKotlinPlugin.apply()")
        project.tasks.matching { it.name in ['compileJava', 'compileKotlin'] }.all {
            def compileTask = it as AbstractCompile
            project.afterEvaluate {
                if (!compileTask.source.empty) {
                    String instrumentName = compileTask.name.replace('compile', 'instrument')

                    ByteBuddyTask byteBuddyTask = project.tasks.create(instrumentName, ByteBuddyTask)
                    byteBuddyTask.group = 'Byte Buddy'
                    byteBuddyTask.description = "Instruments the classes compiled by ${compileTask.name}"

                    byteBuddyTask.entryPoint = EntryPoint.Default.REBASE
                    byteBuddyTask.suffix = ''
                    byteBuddyTask.failOnLiveInitializer = true
                    byteBuddyTask.warnOnEmptyTypeSet = true
                    byteBuddyTask.failFast = false
                    byteBuddyTask.extendedParsing = false
                    byteBuddyTask.discovery = Discovery.NONE
                    byteBuddyTask.threads = 0
                    byteBuddyTask.classFileVersion = ClassFileVersion.JAVA_V17

                    byteBuddyTask.incrementalResolver = IncrementalResolver.ForChangedFiles.INSTANCE

                    // use intermediate 'raw' directory for unprocessed classes
                    Directory classesDir = compileTask.destinationDirectory.get()
                    Directory rawClassesDir = classesDir.dir('../raw/')

                    byteBuddyTask.source = rawClassesDir
                    byteBuddyTask.target = classesDir

                    compileTask.destinationDir = rawClassesDir.asFile

                    byteBuddyTask.classPath.from(project.configurations.compileClasspath + compileTask.destinationDir)

                    byteBuddyTask.transformation {
                        it.plugin = JMoleculesPlugin
                        it.argument({ it.value = byteBuddyTask.classPath.collect({ it.toURI() as String }) })
                        it.argument({ it.value = byteBuddyTask.target.get().asFile.path }) // must serialize as String
                    }

                    // insert task between compile and jar, and before test*
                    byteBuddyTask.dependsOn(compileTask)
                    project.tasks.named(project.sourceSets.main.classesTaskName).configure {
                        dependsOn(byteBuddyTask)
                    }
                }
            }
        }
    }
}
raphw commented

Feel free to add this detection to the official plugin. Gradle is a bit messy in the sense that you need to adjust the build for any JVM language (or more, if there are multiple plugins for it). The JavaPlugin is part of the official Gradle API, but it should not be much work to add something similar for Kotlin or even Scala. It would need to be added here:

https://github.com/raphw/byte-buddy/blob/master/byte-buddy-gradle-plugin/src/main/java/net/bytebuddy/build/gradle/ByteBuddyPlugin.java#L61

I would do it myself but I am not using Kotlin myself and therefore it's not a priority.

hanrw commented

Thanks. will try it first.

raphw/byte-buddy#1284 implies that jmolecules gradle plugin is working with Kotlin by pointing byte buddy to the kotlin output directory. But I still don't get this running. @hanrw could you please provide a working build.gradle.kts? That would be great.

hanrw commented

@jason076 I just did some tests for that but not working well
Here's some code

  1. create a plugin under buildSrc/src/main/groovy/com/bytebuddy/plugin/ByteBuddyKotlinPlugin.groovy
import net.bytebuddy.ClassFileVersion
import net.bytebuddy.build.EntryPoint
import net.bytebuddy.build.gradle.ByteBuddyTask
import net.bytebuddy.build.gradle.Discovery
import net.bytebuddy.build.gradle.IncrementalResolver
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.Directory
import org.gradle.api.tasks.compile.AbstractCompile
import org.jmolecules.bytebuddy.JMoleculesPlugin

class ByteBuddyKotlinPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println ("ByteBuddyKotlinPlugin.apply()")
        project.tasks.matching { it.name in ['compileJava', 'compileScala', 'compileKotlin'] }.all {
            def compileTask = it as AbstractCompile
            project.afterEvaluate {
                if (!compileTask.source.empty) {
                    String instrumentName = compileTask.name.replace('compile', 'instrument')

                    ByteBuddyTask byteBuddyTask = project.tasks.create(instrumentName, ByteBuddyTask)
                    byteBuddyTask.group = 'Byte Buddy'
                    byteBuddyTask.description = "Instruments the classes compiled by ${compileTask.name}"

                    byteBuddyTask.entryPoint = EntryPoint.Default.REBASE
                    byteBuddyTask.suffix = ''
                    byteBuddyTask.failOnLiveInitializer = true
                    byteBuddyTask.warnOnEmptyTypeSet = true
                    byteBuddyTask.failFast = false
                    byteBuddyTask.extendedParsing = false
                    byteBuddyTask.discovery = Discovery.NONE
                    byteBuddyTask.threads = 0
                    byteBuddyTask.classFileVersion = ClassFileVersion.JAVA_V17

                    byteBuddyTask.incrementalResolver = IncrementalResolver.ForChangedFiles.INSTANCE

                    // use intermediate 'raw' directory for unprocessed classes
                    Directory classesDir = compileTask.destinationDirectory.get()
                    Directory rawClassesDir = classesDir.dir('../raw/')

                    byteBuddyTask.source = rawClassesDir
                    byteBuddyTask.target = classesDir

                    compileTask.destinationDir = rawClassesDir.asFile

                    byteBuddyTask.classPath.from(project.configurations.compileClasspath + compileTask.destinationDir)

                    byteBuddyTask.transformation {
                        it.plugin = JMoleculesPlugin
                        it.argument({ it.value = byteBuddyTask.classPath.collect({ it.toURI() as String }) })
                        it.argument({ it.value = byteBuddyTask.target.get().asFile.path }) // must serialize as String
                    }

                    // insert task between compile and jar, and before test*
                    byteBuddyTask.dependsOn(compileTask)
                    project.tasks.named(project.sourceSets.main.classesTaskName).configure {
                        dependsOn(byteBuddyTask)
                    }
                }
            }
        }
    }
}
  1. create buildSrc/src/main/resources/META-INF/gradle-plugins/ByteBuddyKotlinPlugin.properties
    implementation-class=com.bytebuddy.plugin.ByteBuddyKotlinPlugin
  2. apply plugin - build.gradle.kts
plugins {
    ByteBuddyKotlinPlugin
    id("org.jetbrains.kotlin.plugin.allopen") version "1.6.20"
    id("org.springframework.boot") version "2.7.1"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
    kotlin("jvm") version "1.6.20"
    id("org.jetbrains.kotlinx.kover") version "0.5.1"
    kotlin("plugin.spring") version "1.6.21"
    kotlin("plugin.jpa") version "1.6.21"
}
raphw commented

So what the Byte Buddy plugin for Gradle does is that it finds the classes folder of the Java compiler plugin. It then redefines the target directory of the previous plugin. It then depends on the Java compiler plugin, takes the now moved folder as its input and defines the previously defined folder as its output. It then finds all tasks that depend on the Java compiler plugin and makes them depend on itself.

Gradle does not offer a good way to inject a task into the middle of two tasks, that's why this is required, unfortunately. Also, due to incremental compilation, each task needs a clear output folder. This is why - in contrast to Maven - all needs to be implemented tool chain specific. I have never looked into Kotlin, but ideally it would be supported by BB out of the box. BB Gradle already offers discovery for the Android "build flavour" of Gradle and similarly, one could add support for Kotlin. If you wanted to look into this and contribute it to Byte Buddy, I am happy to guide you through.

You would need to start out by adding the KotlinPluginwhere currently, only the JavaPlugin is discovered.
From there, you would need to configure the plugin similarly to the Java plugin using its convention.

This is already implemented using reflection for Java, as Gradle switched from extensions to conventions and I did not want to break Gradle usage for people who do not run the latest Gradle version, so it should look very similar for Kotlin. Your change could therefor start out with something like:

try {
Class<?> kotlin = Class.forName("some.KotlinPlugin");
  project.getPlugins().withType(kotlin, new KotlinPluginConfigurationAction(project));
} catch(ClassNotFoundException exception) {
  getLogger().trace("Did not discover Kotlin plugin", exception);
}

If you give me a first draft of this, and ideally some basic unit test, I can do the rest and round it up.

Your attachment point would likely be the Kotlin base plugin class