vkuzel.com

Generating JAXB classes in Java 17 from Gradle build script

2023-09-11

JAXB, together with XJC generator tool has been removed from Java 11. This article explains one possibility, to use JAXB in Java 17.

  • Adding JAXB dependencies to Java 17 project.
  • Creating a custom XJC Gradle task.

Preparing project build script

In the main project or a module build script, we need to register XJC task. Assuming JAXB files will be generated into a separate directory src/generated/java we also have to add the directory into main source set. Finally, JAXB runtime libraries has to be declared as dependencies.

// build.gradle.kts

// Register XJC task, declared in the `buildSrc` directory.
tasks {
    register<XjcTask>("generateJaxbClasses") {
        sourceDir = file("src/main/resources")
        targetDir = file("src/generated/java")
    }
}

// Add generated directory into the main source set.
sourceSets {
    main {
        java.srcDirs("src/generated/java")
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.glassfish.jaxb:jaxb-runtime:4.0.3")
    implementation("jakarta.xml.bind:jakarta.xml.bind-api:4.0.0")
}

Creating XJC task

The task will be implemented in the included build directory buildSrc. To call XJC from it, dependencies to the XJC libraries has to be added into buildSrc build script.

The XJC task will be implemented in Kotlin, hence kotlin-dsl plugin is applied. But, any JVM language can be used.

// buildSrc/build.gradle.kts

plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.glassfish.jaxb:jaxb-xjc:4.0.3")
    implementation("org.glassfish.jaxb:jaxb-runtime:4.0.3")
}

The XJC task simply reads all schema files (XSD, WSDL) from a source directory and writes generated Java classes into a target directory.

When executed, the task deletes and re-creates the target directory. Then, calls the XJC library (Driver). Common approach is to call XJC indirectly, in a separate Java process by implementing JavaExec task. Compare to the follwing implementation indirect XJC calling is more involved.

// buildSrc/src/main/kotlin/XjcTask

open class XjcTask : DefaultTask() {

    @get:InputDirectory
    open var sourceDir = File("invalid")

    @get:OutputDirectory
    open var targetDir = project.file("invalid")

    @TaskAction
    fun generateSources() {
        targetDir.deleteRecursively()
        targetDir.mkdirs()

        // Consult the documentation for more arguments: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/xjc.html
        val args = arrayOf(
            "-d", targetDir.absolutePath,
            sourceDir.absolutePath,
        )
        val result = Driver.run(args, object : XJCListener() {
            override fun warning(exception: SAXParseException?) = logger.warn("XJC warning", exception)
            override fun error(exception: SAXParseException?) = logger.error("XJC error", exception)
            override fun fatalError(exception: SAXParseException?) = logger.error("XJC fatal error", exception)
            override fun info(exception: SAXParseException?) = logger.info("XJC info", exception)
        })
        if (result != 0) {
            throw GradleException("XJC generator from $sourceDir to $targetDir ended exceptionally!")
        }
    }
}

Finally, the task can be executed.

./gradlew generateJaxbClasses