Reputation: 2093
I have a JavaFX application built with Maven which uses logback-classic
as a SLF4J provider. I am using javafx-maven-plugin
to run the app and to build a jlink image.
When I run the app using mvn javafx:run
, the logging is fine.
When I build an image using mvn javafx:jlink
and run that image, I get the error:
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
It seems like logback-classic
is not being properly included in the image.
I have tried to manually copy logback-classic-1.5.7.jar
and logback-core-1.5.7.jar
into target/image/lib
but it had no effect.
How can I resolve this?
Upvotes: 2
Views: 112
Reputation: 46181
When linking, the module resolution algorithm starts with a set of root modules and then recursively enumerates their requires
directives until all required modules are found (though it appears to skip modules that are only ever requires static
). As far as I can tell, the root modules are specified by the --add-modules
argument and each --launcher
argument (if any). Once these modules are resolved, the algorithm will perform another recursive search for any modules which provides
a service any resolved module uses
, but only if --bind-services
is passed. If a module is not resolved (because it is not a root module, is not directly or indirectly required by a root module, and doesn't provide a service), then it will not be included in the runtime image.
The ch.qos.logback.classic
module is a so-called "provider module". That means its primary purpose is to provide a service that is used by another module and is rarely directly or indirectly required. Thus, it will not be resolved when creating the runtime image with jlink
unless you either pass --bind-services
or explicitly include it via --add-modules
.
The javafx-maven-plugin plugin for Maven unfortunately does not seem to provide a way to specify --add-modules
. Though you can tell it to pass --bind-services
with:
<bindServices>true</bindServices>
Though to get it to include ch.qos.logback.classic
on the module-path when executing javafx:jlink
, it seems you also have to set:
<runtimePathOption>MODULEPATH</runtimePathOption>
However, --bind-services
will include all provider modules of any service used by each resolved module. This can lead to significantly more modules from the JDK being included than necessary. You can control this somewhat by passing an appropriate --limit-modules
argument, but again the plugin doesn't seem to have a way to pass that argument. Plus, using --limit-modules
seems more like a workaround in this case than a genuine solution.
In short, you basically have the following options:
Configure the javafx-maven-plugin to pass --bind-services
and live with the larger-than-necessary runtime image.
Add a requires ch.qos.logback.classic;
directive to your application's module-info descriptor.
Note that requiring a provider module should typically be avoided. Doing so can make it harder to swap providers. However, I suspect requiring ch.qos.logback.classic
won't cause significant issues in your case and is likely the easiest and most efficient solution.
Use a different Maven plugin for creating runtime images, one that gives you more control over the arguments passed to jlink
.
Here is an example configuring javafx-maven-plugin to pass --bind-services
.
Maven 3.9.9
Java 23
JavaFX 23.0.1
SLF4J 2.0.16
Logback Classic 1.5.12
javafx-maven-plugin 0.0.8
module-info.java
module app {
requires javafx.controls;
requires org.slf4j;
exports com.example.app to
javafx.graphics;
}
Main.java
package com.example.app;
import java.util.stream.Collectors;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Main extends Application {
private static final Logger LOG = LoggerFactory.getLogger(Main.class);
@Override
public void init() {
var modules = ModuleLayer.boot()
.modules()
.stream()
.map(Module::getName)
.sorted()
.collect(Collectors.joining("\n ", "\n ", ""));
LOG.info("Resolved modules: {}", modules);
}
@Override
public void start(Stage primaryStage) {
LOG.info("Application start.");
var root = new StackPane(new Label("Hello, World!"));
primaryStage.setScene(new Scene(root, 500, 300));
primaryStage.setTitle("Example");
primaryStage.show();
}
@Override
public void stop() {
LOG.info("Application stop.");
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jfx-slf4j-test</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>23</maven.compiler.release>
<slf4jVersion>2.0.16</slf4jVersion>
<logbackVersion>1.5.12</logbackVersion>
<javafxVersion>23.0.1</javafxVersion>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<runtimePathOption>MODULEPATH</runtimePathOption>
<mainClass>app/com.example.app.Main</mainClass>
<launcher>example</launcher>
<bindServices>true</bindServices>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4jVersion}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logbackVersion}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafxVersion}</version>
</dependency>
</dependencies>
</project>
<PROJECT-ROOT>
| pom.xml
|
\---src
\---main
\---java
| module-info.java
|
\---com
\---example
\---app
Main.java
From executing mvn javafx:jlink
:
... > mvn javafx:jlink
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< com.example:jfx-slf4j-test >---------------------
[INFO] Building jfx-slf4j-test 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] >>> javafx:0.0.8:jlink (default-cli) > process-classes @ jfx-slf4j-test >>>
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ jfx-slf4j-test ---
[INFO] skip non existing resourceDirectory C:\...\jfx-slf4j-test\src\main\resources
[INFO]
[INFO] --- compiler:3.13.0:compile (default-compile) @ jfx-slf4j-test ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 2 source files with javac [debug release 23 module-path] to target\classes
[INFO]
[INFO] <<< javafx:0.0.8:jlink (default-cli) < process-classes @ jfx-slf4j-test <<<
[INFO]
[INFO]
[INFO] --- javafx:0.0.8:jlink (default-cli) @ jfx-slf4j-test ---
Warning: The 0 argument for --compress is deprecated and may be removed in a future release
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.956 s
[INFO] Finished at: 2024-10-27T13:40:10-06:00
[INFO] ------------------------------------------------------------------------
From executing the runtime image via the launcher script:
... > ./target/image/bin/example
13:42:29.164 [JavaFX-Launcher] INFO com.example.app.Main -- Resolved modules:
app
ch.qos.logback.classic
ch.qos.logback.core
java.base
java.compiler
java.datatransfer
java.desktop
java.logging
java.management
java.management.rmi
java.naming
java.prefs
java.rmi
java.security.jgss
java.security.sasl
java.smartcardio
java.xml
java.xml.crypto
javafx.base
javafx.controls
javafx.graphics
jdk.accessibility
jdk.attach
jdk.charsets
jdk.compiler
jdk.crypto.cryptoki
jdk.crypto.mscapi
jdk.editpad
jdk.internal.ed
jdk.internal.jvmstat
jdk.internal.le
jdk.internal.md
jdk.internal.opt
jdk.jartool
jdk.javadoc
jdk.jdeps
jdk.jdi
jdk.jdwp.agent
jdk.jfr
jdk.jlink
jdk.jpackage
jdk.jshell
jdk.jstatd
jdk.localedata
jdk.management
jdk.management.jfr
jdk.naming.dns
jdk.naming.rmi
jdk.security.auth
jdk.security.jgss
jdk.unsupported
jdk.unsupported.desktop
jdk.zipfs
org.slf4j
13:42:29.182 [JavaFX Application Thread] INFO com.example.app.Main -- Application start.
13:42:31.689 [JavaFX Application Thread] INFO com.example.app.Main -- Application stop.
(Note how this approach leads to many more modules in the runtime image than needed)
Upvotes: 4