Jardo
Jardo

Reputation: 2093

Logback not working in a JavaFX image built by JLink

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

Answers (1)

Slaw
Slaw

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:

  1. Configure the javafx-maven-plugin to pass --bind-services and live with the larger-than-necessary runtime image.

  2. 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.

  3. Use a different Maven plugin for creating runtime images, one that gives you more control over the arguments passed to jlink.


Bind Services Example

Here is an example configuring javafx-maven-plugin to pass --bind-services.

Versions

  • 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

Source Code

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.");
  }
}

POM

<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 Structure

<PROJECT-ROOT>
|   pom.xml
|
\---src
    \---main
        \---java
            |   module-info.java
            |
            \---com
                \---example
                    \---app
                            Main.java

Output

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

Related Questions