To make projects compatible with both Java 8 and modular JDKs, a popular strategy
is to compile the project so it produces Java 8 bytecode, and separately compile
the module-info.java
file targeting a modular JDK version.
The Apache Maven documentation includes an example of how to achieve that, but
the Gradle user guide lacks it. This document presents an approach that could
be included in a convention plugin, and requires Gradle 7 or higher (you
could try to adapt it to Gradle 6.x by explicitly enabling inferModulePath
in
compileJava
, but modularity is only a full feature in version 7).
The following snippet configures the compileJava
task and adds three tasks:
-
compileLegacyJava
to compile the codebase with Java 8, exceptmodule-info.java
. -
checkLegacyJava
verifies that a sample project class was effectively compiled towards version 8 bytecode. -
jvmVersionAttribute
sets theorg.gradle.jvm.version
attribute to 8, so the metadata that Gradle generates advertises Java 8 as the minimum required Java version (otherwise it would be 11).
If your project is simple enough you could omit the second task, but sometimes plugins or other tasks can mess with the configuration so it is generally a good idea to keep it.
The module-info.java
file is compiled to Java 11 bytecode, as version 11 is
currently the lowest modular JDK that is still supported:
java {
sourceCompatibility = JavaVersion.VERSION_11
}
compileJava {
includes = ['module-info.java']
dependsOn compileLegacyJava
classpath = sourceSets.main.compileClasspath
}
tasks.register('compileLegacyJava', JavaCompile) {
description = 'Compile to Java 8 bytecode, except module-info'
dependsOn configurations.compileClasspath
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
source = sourceSets.main.java
classpath = sourceSets.main.compileClasspath
destinationDirectory = sourceSets.main.java.destinationDirectory
modularity.inferModulePath = false
excludes = ['module-info.java']
}
// Check bytecode version, in case some other task screws it
tasks.register('checkLegacyJava') {
description = 'Check that classes are Java 8 bytecode (except module-info)'
enabled = enabled && !project.getPluginManager().hasPlugin('eclipse')
def classdir = sourceSets.main.output.classesDirs.files.stream().findAny().get()
def classfiles = fileTree(classdir).matching({it.exclude('module-info.class')}).files
doFirst() {
if (!classfiles.isEmpty()) {
def classfile = classfiles.stream().findAny().get()
if (classfile != null) {
def classbytes = classfile.bytes
def bcversion = classbytes[6] * 128 + classbytes[7]
if (bcversion != 52) {
throw new GradleException("Bytecode on " + classfile +
" is not valid Java 8. Version should be 52, instead is " + bcversion)
}
}
}
}
}
// Set the 'org.gradle.jvm.version' attribute to 8
tasks.register('jvmVersionAttribute') {
description = "Set the correct 'org.gradle.jvm.version' attribute"
dependsOn compileJava
if (!project.getPluginManager().hasPlugin('eclipse')) {
def jvmVersionAttribute = Attribute.of('org.gradle.jvm.version', Integer)
configurations.each {
if (it.canBeConsumed) {
def categoryAttr = it.attributes.getAttribute(Category.CATEGORY_ATTRIBUTE)
if (categoryAttr != null && categoryAttr.name == Category.LIBRARY) {
def usageAttr = it.attributes.getAttribute(Usage.USAGE_ATTRIBUTE)
if (usageAttr != null && (usageAttr.name == Usage.JAVA_API
|| usageAttr.name == Usage.JAVA_RUNTIME)) {
it.attributes.attribute(jvmVersionAttribute, 8)
}
}
}
}
}
}
classes.dependsOn jvmVersionAttribute
classes.finalizedBy checkLegacyJava
jar.dependsOn checkLegacyJava
If you are configuring a multi-module build which has
dependency variants,
you should put the jvmVersionAttribute
dependency declaration in the same file(s)
where the variants are declared, see "Usage in multi-module builds".
If your project has no dependency variants, you can omit the
jvmVersionAttribute
task and instead use this:
configurations {
def jvmVersionAttribute = Attribute.of('org.gradle.jvm.version', Integer)
apiElements {
attributes {
attribute(jvmVersionAttribute, 8)
}
}
runtimeElements {
attributes {
attribute(jvmVersionAttribute, 8)
}
}
}
In a multi-module project, you may want to include the task declarations in a
convention plugin
(those plugins are often located in ${rootDir}/buildSrc/src/main/groovy
, if
you use Groovy scripts).
If you declare dependency variants in your subprojects (as opposed to doing that
in the convention plugin), you need to remove the classes.dependsOn jvmVersionAttribute
dependency declaration from the plugin, and instead put them on each
subproject's build.gradle
file:
classes.dependsOn jvmVersionAttribute
In complex projects that have multiple customized tasks, you may have to adjust
some of those tasks to accommodate the above compileLegacyJava
compilation
task. For example, in those cases sometimes it is a good idea to add a
dependsOn jvmVersionAttribute
to checkLegacyJava
.
Similarly, if your Java compiler needs a specific configuration you may need to modify the task accordingly, preferably using a generic block that modifies all the relevant tasks, for example:
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
}
The code presented in this gist works in most cases but is not intended as an universal, self-contained solution. Customizations may be required.
When you import a project into the Eclipse IDE (or execute
Gradle > Refresh Gradle Project
), it is generally a good idea to run a
command-line build first.