javaspringkotlinjarjpackage

Error: Could not find or load main class Caused by: java.lang.ClassNotFoundException with jpackage


Hi this is my first question! I'm trying to build a standalone kotlin - compose - spring app. My client requested this app for windows so I'd like to create an installer that includes java runtime (my client doesn't have it and wants it to run on its own).

The problem is I can't seem to get it right with jpackage. This is what my gradle looks like, including the jpackage command I'm using:

plugins {
    kotlin("jvm") version "1.9.10"
    kotlin("plugin.spring") version "1.9.10"
    id("org.springframework.boot") version "3.4.1"
    id("io.spring.dependency-management") version "1.1.7"
    kotlin("plugin.jpa") version "1.9.10"

//  compose
    id("org.jetbrains.compose") version "1.5.10"

    // kotlin - spring
//  id("kotlin") version "1.9.10"
//  id("kotlin-spring") version "1.9.10"
}

group = "com.tfra"
var appName = "MechanicManagement"
version = "0.0.1"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

configurations {
    compileOnly {
        extendsFrom(configurations.annotationProcessor.get())
    }
}

repositories {
    google()
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-validation")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    compileOnly("org.projectlombok:lombok")
//  runtimeOnly("org.postgresql:postgresql")
    annotationProcessor("org.projectlombok:lombok")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")

    implementation(compose.desktop.currentOs)
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-web")
    // https://mvnrepository.com/artifact/org.postgresql/postgresql
    implementation("org.postgresql:postgresql:42.7.5")
    // https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
//  implementation("com.mysql:mysql-connector-j:9.1.0")

}

compose.desktop {
    application {
        mainClass = "com.tfra.mechanic_management.MechanicManagementApplication"
    }
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll("-Xjsr305=strict")
    }
}

allOpen {
    annotation("jakarta.persistence.Entity")
    annotation("jakarta.persistence.MappedSuperclass")
    annotation("jakarta.persistence.Embeddable")
}

springBoot {
    mainClass = "com.tfra.mechanic_management.MechanicManagementApplicationKt"
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
    archiveFileName.set("${appName}-${version}.jar")

    manifest {
        attributes["Main-Class"] = "com.tfra.mechanic_management.MechanicManagementApplicationKt"
    }

//   Aggiunge le dipendenze al JAR
    doFirst {
        // Elimina eventuali duplicati
        duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    }

    // Aggiunge tutte le dipendenze del classpath al JAR
    from({
        configurations.runtimeClasspath.get().filter { it.exists() }.map {
            if (it.isDirectory) it else zipTree(it)
        }
    })
}


tasks.register<Exec>("generateRuntimeImage") {
    group = "build"
    description = "Generates a custom runtime image"

    val runtimeDir = layout.buildDirectory.dir("my-runtime").get().asFile

    doFirst {
        if (runtimeDir.exists()) {
            runtimeDir.deleteRecursively() // Cancella la directory esistente
        }
    }

    commandLine(
        "jlink",
        "--module-path", "${System.getProperty("java.home")}/jmods;build/libs",
        "--add-modules", "java.base,java.desktop,java.sql,java.naming",
        "--output", layout.buildDirectory.dir("my-runtime").get().asFile.absolutePath
    )
}



tasks.register<Exec>("createInstaller") {
    group = "distribution"
    description = "Creates a native installer for the application"

    dependsOn("bootJar") // Assicura che bootJar venga eseguito prima
//  dependsOn("jar")

    val outputDir = layout.buildDirectory.dir("installer").get().asFile.absolutePath
    val jarPath = layout.buildDirectory.file("libs/${appName}-${version}.jar").get().asFile.absolutePath

    val javaHome = System.getProperty("java.home")

    doFirst {
        mkdir(outputDir)
    }

    dependsOn("generateRuntimeImage")

    commandLine(
        "$javaHome/bin/jpackage",
        "--type", "exe",
        "--input", "build/libs",
        "--main-jar", jarPath,
        "--name", appName,
        "--main-class", "com.tfra.mechanic_management.MechanicManagementApplicationKt",
        "--dest", outputDir,
        "--app-version", version.toString(),
        "--icon", "src/main/resources/car_icon.ico",
        "--java-options", "-Djava.awt.headless=false",
        "--java-options", "-Dlogging.file=app.log",
        "--resource-dir", "src/main/resources",
        "--runtime-image", layout.buildDirectory.dir("my-runtime").get().asFile.absolutePath
    )
}

this is my kotlin application class, MechanicManagementApplication.kt :

@SpringBootApplication
@EnableJpaRepositories(basePackages = ["com.tfra.mechanic_management.repository.jpa"])
@EntityScan(basePackages = ["com.tfra.mechanic_management.repository.entity"])
@ComponentScan(basePackages = ["com.tfra"])
//@SpringBootApplication(exclude = [DataSourceAutoConfiguration::class])
class MechanicManagementApplication

fun main(args: Array<String>) {
//  val context = runApplication<MechanicManagementApplication>()
//  application {
//      AppUI(context)
//  }
    val context = runApplication<MechanicManagementApplication>(*args)

    application {
        Window(
            onCloseRequest = ::exitApplication,
            title = "Mechanic Management"
        ) {
            MaterialTheme { // Fornisce il tema di Material Design
                AppUI(context)
            }
        }
    }
}

when I run the generated jar or the exe from the installer (which, also, I have to execute in Windows Sandbox because otherwise it won't execute and I have to terminate using task manager), I get the following:

Error: Could not find or load main class com.tfra.mechanic_management.MechanicManagementApplicationKt
Caused by: java.lang.ClassNotFoundException: com.tfra.mechanic_management.MechanicManagementApplicationKt

What am I doing wrong?


Solution

  • You are trying to outsmart Spring Boot (and maybe even jpackage?).

    tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
        archiveFileName.set("${appName}-${version}.jar")
    
        manifest {
            attributes["Main-Class"] = "com.tfra.mechanic_management.MechanicManagementApplicationKt"
        }
    
    //   Aggiunge le dipendenze al JAR
        doFirst {
            // Elimina eventuali duplicati
            duplicatesStrategy = DuplicatesStrategy.EXCLUDE
        }
    
        // Aggiunge tutte le dipendenze del classpath al JAR
        from({
            configurations.runtimeClasspath.get().filter { it.exists() }.map {
                if (it.isDirectory) it else zipTree(it)
            }
        })
    }
    

    This task is basically replacing what Spring Boot already does and in a bad way as well. This will miss out on things.

    Instead just ditch this restructuring of the Spring Boot bootJar task and let it do its thing instead of trying to be smarter. As you now have a jar which is executable (it contains a MANIFEST.MF with all the proper entries) you can also remove the --main-class from the createInstaller task.

    tasks.register<Exec>("createInstaller") {
        group = "distribution"
        description = "Creates a native installer for the application"
    
        dependsOn("bootJar") // Assicura che bootJar venga eseguito prima
    //  dependsOn("jar")
    
        val outputDir = layout.buildDirectory.dir("installer").get().asFile.absolutePath
        val jarPath = layout.buildDirectory.file("libs/${appName}-${version}.jar").get().asFile.absolutePath
    
        val javaHome = System.getProperty("java.home")
    
        doFirst {
            mkdir(outputDir)
        }
    
        dependsOn("generateRuntimeImage")
    
        commandLine(
            "$javaHome/bin/jpackage",
            "--type", "exe",
            "--input", "build/libs",
            "--main-jar", jarPath,
            "--name", appName,
            "--dest", outputDir,
            "--app-version", version.toString(),
            "--icon", "src/main/resources/car_icon.ico",
            "--java-options", "-Djava.awt.headless=false",
            "--java-options", "-Dlogging.file=app.log",
            "--resource-dir", "src/main/resources",
            "--runtime-image", layout.buildDirectory.dir("my-runtime").get().asFile.absolutePath
        )
    }
    

    Now by making things simpler it will work.