xmolecules / jmolecules-examples

Example projects for jMolecules
Apache License 2.0
15 stars 9 forks source link

jmolecules-kotlin with build.gradle.kts not working #6

Closed hanrw closed 2 years ago

hanrw commented 2 years ago

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 2 years ago

and reproducer project - issue-6.zip

odrotbohm commented 2 years ago

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 2 years ago

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 2 years ago

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 2 years ago

Thanks. will try it first.

jason076 commented 1 year ago

https://github.com/raphw/byte-buddy/issues/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 1 year ago

@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 {

@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)
                }
            }
        }
    }
}

}

2. create buildSrc/src/main/resources/META-INF/gradle-plugins/ByteBuddyKotlinPlugin.properties
`implementation-class=com.bytebuddy.plugin.ByteBuddyKotlinPlugin`
3. 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 1 year ago

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