Add dependecies to runtime image with Gradle

2019-06-24 17:51发布

问题:

I don't know how to add dependency. My module needs Log4j. I added requirement to module info. I added also to gradle dependencies. I can run project, but I can't create custom runtime image.

plugins {
    id 'java'
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.5'
}

group 'eu.sample'
version '2.0'


repositories {
    mavenCentral()
}

javafx {
    modules = [ 'javafx.controls', 'javafx.fxml' ]
}

mainClassName = "$moduleName/eu.sample.app.Main"

def lin_java_home = hasProperty('org.gradle.java.home') ? getProperty('org.gradle.java.home') : System.getenv('JAVA_HOME')
def lin_fx_jmods = hasProperty('linux.fx.mods') ? getProperty('linux.fx.mods') : System.getenv('PATH_TO_FX_MODS_LIN')

def win_java_home = hasProperty('windows.java.home') ? getProperty('windows.java.home') : System.getenv('JAVA_HOME_WIN')
def win_fx_jmods = hasProperty('windows.fx.mods') ? getProperty('windows.fx.mods') : System.getenv('PATH_TO_FX_MODS_WIN')

dependencies {
    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
}

task jlink(type: Exec) {
    dependsOn 'clean'
    dependsOn 'jar'

    workingDir 'build'

    if (lin_java_home == null) {
        throw new RuntimeException("java_home is not defined.")
    }
    if (lin_fx_jmods == null) {
        throw new RuntimeException("fx_jmods is not defined.")
    }
    commandLine "${lin_java_home}/bin/jlink", '--module-path', "libs${File.pathSeparatorChar}${lin_fx_jmods}",
            '--add-modules', "${moduleName}", '--output', "${moduleName}", '--strip-debug',
            '--compress', '2', '--no-header-files', '--no-man-pages'
}

task jlinkWin(type: Exec) {
    dependsOn 'clean'
    dependsOn 'jar'


    workingDir 'build'

    if (win_java_home == null) {
        throw new RuntimeException("java_home is not defined.")
    }
    if (win_fx_jmods == null) {
        throw new RuntimeException("fx_jmods is not defined.")
    }
    commandLine "${lin_java_home}/bin/jlink", '--module-path', 
            "${win_java_home}/jmods${File.pathSeparatorChar}libs${File.pathSeparatorChar}${win_fx_jmods}",
            '--add-modules', "${moduleName}", '--output', "${moduleName}", '--strip-debug',
            '--compress', '2', '--no-header-files', '--no-man-pages'
}

When I fire task jlink I get:

Error: Module org.apache.logging.log4j not found, required by app

I checked libs directory in build and there is not log4j jar. How to tell gradle to add dependencies to jlink task?

回答1:

Problem

This is what you have in your jlink task:

'--module-path', "libs${File.pathSeparatorChar}${fx_jmods}"

Whats means that you are adding the dependencies from:

  • libs: basically the jar of your module helloFX. This is a result of the jar task, that only includes the classes and resources of your project, but not its dependencies.

  • fx_jmods: That is the path to the JavaFX jmods.

But when you run, you get this error:

Error: Module org.apache.logging.log4j not found, required by app

The error means that the module-path for the jlink command is not complete, and it can't resolve all the required dependencies. As mentioned above, we are only including the module.jar and the JavaFX (jmods) jars, but not the log4j.jar.

So we need to find a way to add that jar to the module-path.

There are a few possible solutions to include third party dependencies in your custom image.

Solution 1

We have to modify the jlink task, to include the existing dependencies in our runtime configuration.

The most immediate solution is finding where the logj4 jars are stored by gradle, in the local .gradle repository.

'--module-path', "libs${File.pathSeparatorChar}${fx_jmods}: \
   /Users/<user>/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-api/2.11.1/268..a10/log4j-api-2.11.1.jar: \
   /Users/<user>/.gradle/caches/modules-2/files-2.1/org.apache.logging.log4j/log4j-core/2.11.1/59..e4/log4j-core-2.11.1.jar"

While this solution works, it is not the most convenient, of course, as it depends on local paths from the user.

Solution 2

A better solution, can be done adding a task to copy the runtime dependencies, that are resolved directly by gradle, into the libs folder like:

task libs(type: Copy) {
    into 'build/libs/'
    from configurations.runtime
}

and then calling this task from the jlink task:

task jlink(type: Exec) {
    dependsOn 'clean'
    dependsOn 'jar'
    dependsOn 'libs'
    ...
}

If you run ./gradlew jlink and check the libs folder you should find something like this:

build/libs/hellofx.jar
build/libs/javafx-base-11.0.1.jar
build/libs/javafx-base-11.0.1-$platform.jar
build/libs/javafx-graphics-11.0.1.jar
build/libs/javafx-graphics-11.0.1-$platform.jar
build/libs/javafx-controls-11.0.1.jar
build/libs/javafx-controls-11.0.1-$platform.jar
build/libs/javafx-fxml-11.0.1.jar
build/libs/javafx-fxml-11.0.1-$platform.jar
build/libs/log4j-api-2.11.1.jar
build/libs/log4j-core-2.11.1.jar

where $platform is your running platform.

Notice that the libs folder now contains all the dependencies that are required for the module path, and also that the JavaFX-*-$platform jars contain the native libraries, therefore, the jmods are not needed anymore in the module-path option. This will be enough:

'--module-path', "libs"

so your command line will be:

commandLine "${java_home}/bin/jlink", '--module-path', "libs",
        '--add-modules', "${moduleName}", '--output', "${moduleName}", '--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages'

You can run your jlink task now successfully.

Solution 3

As suggested by @madhead comment, you can use another plugin for the jlink task: the so called badass-jlink-plugin.

Modify your build to something like:

plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.5'
    id 'org.beryx.jlink' version '2.1.8'
}

repositories {
    mavenCentral()
}

dependencies {
    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.11.1'
}

javafx {
    modules = ['javafx.controls', 'javafx.fxml']
}

mainClassName = "${moduleName}/eu.sample.app.Main"

jlink {
    options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
    launcher {
        name = 'helloFX'
    }
}

Note also that the plugin has an option to create image for other platforms (see targetPlatform).

EDIT about log4j

As mentioned in the comments, the log4j dependency doesn't play well with modules yet. There is an open issue at the Apache Issue tracker:

Currently based on the given automatic modules you can't use log4j-core in a project where you like to use jlink to produce a runtime image.

I've added this to my main class:

LogManager.getLogger(MainApp.class).info("hellofx!");

and I've added the log4j2.xml file to src/main/resources.

Running ./gradlew run, works:

> Task :run
18:24:26.362 [JavaFX Application Thread] INFO  eu.sample.app.Main - hellofx!

But, running jlink from Solution 2, creates the custom image, I can verify that the xml file is included, but when running from the image I get:

build/hellofx/bin/java -m hellofx/eu.sample.app.Main
ERROR StatusLogger Log4j2 could not find a logging implementation. \
    Please add log4j-core to the classpath. Using SimpleLogger to log to the console...

And as mentioned, running jlink with the plugin from Solution 3 fails at the createMergedModule task:

 error: package org.apache.logging.log4j.spi is not visible
   provides org.apache.logging.log4j.spi.Provider with org.apache.logging.log4j.core.impl.Log4jProvider;

[See EDIT(2), this has been fixed since version 2.1.9]

Alternative

At this point, a possible alternative could be use Slf4j instead. There is a sample listed from the plugin documentation that uses it successfully.

In summary this is required:

Gradle file:

dependencies {
    compile 'org.slf4j:slf4j-api:1.8.0-beta2'
    compile('ch.qos.logback:logback-classic:1.3.0-alpha4') {
        exclude module: "activation"
    }
}

Module-info:

requires org.slf4j;
requires ch.qos.logback.classic;
requires java.naming;

Main class:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger logger = LoggerFactory.getLogger(MainApp.class);

...
logger.info("hellofx!");

logging.properties from here and logback.xml from here, with your main class.

Both running ./gradlew run or ./gradlew jlink, and build/image/bin/HelloFX work, and the message is logged to the console.

EDIT (2)

After reporting the issue to the badass-jlink-plugin's issue tracker, this was solved, and since version 2.1.19 it should work fine creating a custom image.

Since log4j is multi-release jar, there is one thing required:

plugins {
    id 'application'
    id 'org.openjfx.javafxplugin' version '0.0.5'
    id 'org.beryx.jlink' version '2.1.9'
}

...

jlink {
    options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
    launcher {
       name = 'helloFX'
    }
    forceMerge('log4j-api')     // <---- this is required for Log4j
}

See a full working sample here.