/extra-java-module-info

sim fork

Primary LanguageGroovyApache License 2.0Apache-2.0

Extra Java Module Info Gradle plugin

Build Status Gradle Plugin Portal

A Gradle 6.8+ plugin to use legacy Java libraries as Java Modules in a modular Java project.

This GradleX plugin is maintained by me, Jendrik Johannes. I offer consulting and training for Gradle and/or the Java Module System - please reach out if you are interested. There is also my YouTube channel on Gradle topics.

Special thanks goes to Ihor Herasymenko who has been contributing many features and fixes to this plugin!

If you have a suggestion or a question, please open an issue.

There is a CHANGELOG.md.

Java Modules with Gradle

If you plan to build Java Modules with Gradle, you should consider using these plugins on top of Gradle core:

Here is a sample that shows all plugins in combination.

Full Java Module System Project Setup is a full-fledged Java Module System project setup using these plugins.

How to use this plugin

This plugin allows you to add module information to a Java library that does not have any. If you do that, you can give it a proper module name and Gradle can pick it up to put it on the module path during compilation, testing and execution.

The plugin should be applied to all subprojects of your multi-project build. It is recommended to use a convention plugin for that.

Plugin dependency

Add this to the build file of your convention plugin's build (e.g. gradle/plugins/build.gradle(.kts) or buildSrc/build.gradle(.kts)).

dependencies {
    implementation("org.gradlex:extra-java-module-info:1.4")
}

Defining extra module information

In your convention plugin, apply the plugin and define the additional module info:

plugins {
    ...
    id("org.gradlex.extra-java-module-info")
}

// add module information for all direct and transitive dependencies that are not modules
extraJavaModuleInfo {
    // failOnMissingModuleInfo.set(false)
    module("commons-beanutils:commons-beanutils", "org.apache.commons.beanutils") {
        exports("org.apache.commons.beanutils")
        // exportAllPackages()
        
        requiresTransitive("org.apache.commons.logging")
        requires("java.sql")
        requires("java.desktop")
        
        // closeModule()
        // opens("org.apache.commons.beanutils")
        
        // requiresTransitive(...)
        // requiresStatic(...)
        
        // requireAllDefinedDependencies()
    }
    module("commons-cli:commons-cli", "org.apache.commons.cli") {
        exports("org.apache.commons.cli")
    }
    module("commons-collections:commons-collections", "org.apache.commons.collections")
    automaticModule("commons-logging:commons-logging", "org.apache.commons.logging")
}

Dependencies in build files

Now dependencies defined in your build files are all treated as modules if enough extra information was provided. For example:

dependencies {
    implementation("com.google.code.gson:gson:2.8.6")           // real module
    implementation("net.bytebuddy:byte-buddy:1.10.9")           // real module with multi-release jar
    implementation("org.apache.commons:commons-lang3:3.10")     // automatic module
    implementation("commons-beanutils:commons-beanutils:1.9.4") // plain library (also brings in other libraries transitively)
    implementation("commons-cli:commons-cli:1.4")               // plain library        
}

Sample uses Gradle's Kotlin DSL (build.gradle.kts file). The Groovy DSL syntax is similar.

FAQ

How do I deactivate the plugin functionality for a certain classpath?

This can be useful for the test classpath if it should be used for unit testing on the classpath (rather than the module path). If you use the shadow plugin and encounter this issue, you can deactivate it for the runtime classpath as the module information is irrelevant for a fat Jar in any case.

Kotlin DSL

configurations {
    runtimeClasspath { // testRuntimeClasspath, testCompileClasspath, ... 
        attributes { attribute(Attribute.of("javaModule", Boolean::class.javaObjectType), false) }
    }
}

Groovy DSL

configurations {
    runtimeClasspath { // testRuntimeClasspath, testCompileClasspath, ... 
        attributes { attribute(Attribute.of("javaModule", Boolean), false) }
    }
}

How do I add provides ... with ... declarations to the module-info.class descriptor?

The plugin will automatically retrofit all the available META-INF/services/* descriptors into module-info.class for you. The META-INF/services/* descriptors will be preserved so that a transformed JAR will continue to work if it is placed on the classpath.

The plugin also allows you to ignore some unwanted services from being automatically converted into provides .. with ... declarations.

extraJavaModuleInfo {               
    module("groovy-all-2.4.15.jar", "groovy.all", "2.4.15") {
       requiresTransitive("java.scripting")
       requires("java.logging")
       requires("java.desktop")
       ignoreServiceProvider("org.codehaus.groovy.runtime.ExtensionModule")
       ignoreServiceProvider("org.codehaus.groovy.plugins.Runners")
       ignoreServiceProvider("org.codehaus.groovy.source.Extensions")
    }
}

Should I use real modules or automatic modules?

Only if you use real modules (Jars with module-info.class) everywhere you can use all features of the Java Module System (see e.g. #38 for why it may be problematic to depend on an automatic module). Still, using automatic modules is more convenient if you need to work with a lot of legacy libraries, because you do not need to define exports and requires directives. Alternatively though, this plugin offers a way to define a real module, without defining all of those directives explicitly:

extraJavaModuleInfo {
    module("org.apache.httpcomponents:httpclient", "org.apache.httpcomponents.httpclient") {
        exportAllPackages() // Adds an `exports` for each package found in the Jar
        requireAllDefinedDependencies() // Adds `requires (transitive|static)` directives based on dependencies defined in the component's metadata
    }
}

What do I do in a 'split package' situation?

The Java Module System does not allow the same package to be used in more than one module. This is an issue with legacy libraries, where it was common practice to use the same package in multiple Jars. This plugin offers the option to merge multiple Jars into one in such situations:

 extraJavaModuleInfo {
    module("org.apache.zookeeper:zookeeper", "org.apache.zookeeper") {
        mergeJar("org.apache.zookeeper:zookeeper-jute")
        
        // ...
    }
    automaticModule("org.slf4j:slf4j-api", "org.slf4j") {
        mergeJar("org.slf4j:slf4j-ext")
    }
}

Note: The merged Jar will include the first appearance of duplicated files (like the MANIFEST.MF).

Disclaimer

Gradle and the Gradle logo are trademarks of Gradle, Inc. The GradleX project is not endorsed by, affiliated with, or associated with Gradle or Gradle, Inc. in any way.