gradlejavafxserviceloader

ServiceLoader does not locate implementation when Gradle javafxplugin is used


I am implementing a program where the core part is separated from the GUI and loaded at runtime as a service. I am struggling for weeks to have the implementation discovered at runtime. To try isolating the problem, I picked up a minimal ServiceLoader example from here https://reflectoring.io/service-provider-interface and inflated it into my project structure. I got the the conclusion that the javaxplugin is messing up something. The plugin is required for the GUI of my project, but is not required to run the code of the ServiceLoader. I am using version 0.0.10 of org.openjfx.javafxplugin, the last version is 0.0.13 but this causes the additional problem that the main class cannot be found anymore, so I am staying with the older version for the moment. If the plugin is not requested in the build.gradle, the ServiceLoader code works, the implementation is loaded and the program gives the expected output. When the javaxplugin is requested in the build.gradle, the program does not work anymore.

Does anybody have a suggestion? I am really stuck because this is a JavaFX application and I need that plugin.

The project is a Gradle project with 3 subprojects (modules): the api, the implementation (core) and the application (GUI). The relevant files are here below.

API module-info.java:

module tlapi {
  exports com.chesolver.spi;
  exports com.chesolver;
}

The strange thing here is that if I enable the javafx pluging, the compiler rises the error com.chesolver.spi.Library: module tlapi does not declare 'uses', which appears pretty wierd to me,since module tlapi is the api and com.chesolver.spi.Library is part of the interface contained in this module.

API build.gradle:

plugins {
    id 'tubesheetlayout.java-library-conventions'
}

core module-info:

module tlcore {
    requires tlapi;
}

core build.gradle:

plugins {
    id 'tubesheetlayout.java-library-conventions'
    id 'org.javamodularity.moduleplugin' version '1.8.9'
}

dependencies {
    implementation project(':api')
}

application module-info:

module tlclient {
    requires tlapi;
}

application build.gradle:

plugins {
    id 'tubesheetlayout.java-application-conventions'

    // *NOTICE* if uncommented the ServiceLoader code does not work
    //id 'org.openjfx.javafxplugin' version '0.0.13'
    
    //id 'org.javamodularity.moduleplugin' version '1.8.9'
}

dependencies {
    implementation project(':api')
    implementation project(':core')
}

application {
    mainClass = "com.chesolver.library.LibraryClient"
}

Solution

  • Your main issue (incorrectly specifying service modules) and resources on how to fix it

    For a Java platform modular application, you need to use Java Platform modular service definitions in your modules. This means using the provides and uses statements in your module-info.java.

    Your code, and the service tutorial you linked, don't integrate services into the Java platform module system. The Maven module system discussed in the tutorial (and probably Gradle modules too), is something completely different.

    To learn about modular services, study the "Services" sections in:

    Baeldung provides example code for a modular service tutorial, which is simpler than what I have in this answer. But the Baeldung tutorial doesn't demonstrate binding multiple service implementations, jlinking, or using the loaded service modules from a JavaFX application, which is why I added an example for those things here.

    Key Advice

    My suggestion about this is: don't use the service mechanism unless you know you need it.

    Example Solution

    I know this question is about Gradle specifically, but I don't really use that tool. I will provide an alternate solution using Maven. Some aspects of it will carry directly over to Gradle, and others you will need to adapt.

    The solution consists of a multi-module Maven project. Each of the submodules corresponds to a Java platform module. There is a parent pom.xml to specify all of the child modules and all of the child modules inherit from the parent.

    The submodules involved are these:

    shape provider

    Select the shape factory service provider from the combo box, then click "Create Shape" and the selected provider will be used to generate a shape, which will then be displayed.

    I'll post the code here, unfortunately, there is a lot of it :-(

    Building and running in Idea

    You can import the maven project from the root directory into the Idea IDE. The project will load as a single Idea project, with multiple Idea project modules. When you run the main ShapeApplication class from the IDE, the IDE will automatically build all the modules and provide the services to your application.

    Building and running from the command line

    To build everything, run mvn clean install on the root of the project. To create a jlinked app change to the shape-app directory and run mvn javafx:jlink.

    $ tree
    .
    ├── circle-provider
    │   ├── circle-provider.iml
    │   ├── pom.xml
    │   └── src
    │       └── main
    │           └── java
    │               ├── com
    │               │   └── example
    │               │       └── shapeservice
    │               │           └── circleprovider
    │               │               └── CircleProvider.java
    │               └── module-info.java
    ├── pom.xml
    ├── shape-app
    │   ├── pom.xml
    │   ├── shape-app.iml
    │   └── src
    │       └── main
    │           └── java
    │               ├── com
    │               │   └── example
    │               │       └── shapeapp
    │               │           └── ShapeApplication.java
    │               └── module-info.java
    ├── shape-service
    │   ├── pom.xml
    │   ├── shape-service.iml
    │   └── src
    │       └── main
    │           └── java
    │               ├── com
    │               │   └── example
    │               │       └── shapeservice
    │               │           └── ShapeFactory.java
    │               └── module-info.java
    ├── shapes.iml
    └── square-provider
        ├── pom.xml
        ├── square-provider.iml
        └── src
            └── main
                └── java
                    ├── com
                    │   └── example
                    │       └── shapeservice
                    │           └── squareprovider
                    │               └── SquareProvider.java
                    └── module-info.java
    

    The .iml are just idea module project files, you can ignore them.

    Parent pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <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>shapes</artifactId>
        <version>1.0-SNAPSHOT</version>
        <packaging>pom</packaging>
        <name>shapes</name>
    
        <modules>
            <module>shape-service</module>
            <module>circle-provider</module>
            <module>square-provider</module>
            <module>shape-app</module>
        </modules>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <javafx.version>19</javafx.version>
        </properties>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.10.1</version>
                    <configuration>
                        <source>19</source>
                        <target>19</target>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    shape-service

    module com.example.shapeservice {
        requires javafx.graphics;
        exports com.example.shapeservice;
    }
    

    package com.example.shapeservice;
    
    import javafx.scene.shape.Shape;
    
    public interface ShapeFactory {
        double PREF_SHAPE_SIZE = 40;
    
        Shape createShape();
    }
    

    <?xml version="1.0" encoding="UTF-8"?>
    <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>
    
        <artifactId>shape-service</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <parent>
            <groupId>com.example</groupId>
            <artifactId>shapes</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-graphics</artifactId>
                <version>${javafx.version}</version>
            </dependency>
        </dependencies>
    </project>
    

    circle-provider

    module com.example.shapeservice.circleprovider {
        requires javafx.graphics;
        requires com.example.shapeservice;
    
        provides com.example.shapeservice.ShapeFactory
                with com.example.shapeservice.circleprovider.CircleProvider;
    }
    

    package com.example.shapeservice.circleprovider;
    
    import com.example.shapeservice.ShapeFactory;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Circle;
    import javafx.scene.shape.Shape;
    
    import java.util.concurrent.ThreadLocalRandom;
    
    public class CircleProvider implements ShapeFactory {
        private static final Color[] colors = {
                Color.RED, Color.ORANGE, Color.YELLOW, Color.GREEN, Color.BLUE, Color.VIOLET
        };
    
        @Override
        public Shape createShape() {
            return new Circle(
                    PREF_SHAPE_SIZE / 2,
                    randomColor()
            );
        }
    
        private static Color randomColor() {
            return colors[ThreadLocalRandom.current().nextInt(colors.length)];
        }
    }
    

    <?xml version="1.0" encoding="UTF-8"?>
    <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>
    
        <artifactId>circle-provider</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <parent>
            <groupId>com.example</groupId>
            <artifactId>shapes</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-graphics</artifactId>
                <version>${javafx.version}</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>shape-service</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    </project>
    

    square-provider

    module com.example.shapeservice.squareprovider {
        requires javafx.graphics;
        requires com.example.shapeservice;
    
        provides com.example.shapeservice.ShapeFactory
                with com.example.shapeservice.squareprovider.SquareProvider;
    }
    

    package com.example.shapeservice.squareprovider;
    
    import com.example.shapeservice.ShapeFactory;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Rectangle;
    import javafx.scene.shape.Shape;
    
    import java.util.concurrent.ThreadLocalRandom;
    
    public class SquareProvider implements ShapeFactory {
        private static final Color[] colors = {
                Color.CYAN, Color.MAGENTA, Color.YELLOW, Color.BLACK
        };
    
        @Override
        public Shape createShape() {
            return new Rectangle(
                    PREF_SHAPE_SIZE, PREF_SHAPE_SIZE,
                    randomColor()
            );
        }
    
        private static Color randomColor() {
            return colors[ThreadLocalRandom.current().nextInt(colors.length)];
        }
    }
    

    <?xml version="1.0" encoding="UTF-8"?>
    <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>
    
        <artifactId>square-provider</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <parent>
            <groupId>com.example</groupId>
            <artifactId>shapes</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-graphics</artifactId>
                <version>${javafx.version}</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>shape-service</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    </project>
    

    shape-app

    module com.example.shapeapp {
        requires javafx.controls;
        requires com.example.shapeservice;
    
        uses com.example.shapeservice.ShapeFactory;
    
        exports com.example.shapeapp;
    }
    

    package com.example.shapeapp;
    
    import com.example.shapeservice.ShapeFactory;
    import javafx.application.Application;
    import javafx.collections.*;
    import javafx.geometry.*;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.layout.*;
    import javafx.scene.shape.Shape;
    import javafx.stage.Stage;
    
    import java.util.Comparator;
    import java.util.ServiceLoader;
    import java.util.stream.Collectors;
    
    import static com.example.shapeservice.ShapeFactory.PREF_SHAPE_SIZE;
    
    public class ShapeApplication extends Application {
        @Override
        public void start(Stage stage) {
            ObservableList<ShapeFactory> shapeFactories = loadShapeFactories();
            stage.setScene(new Scene(createUI(shapeFactories)));
            stage.show();
        }
    
        private ObservableList<ShapeFactory> loadShapeFactories() {
            ServiceLoader<ShapeFactory> loader = ServiceLoader.load(ShapeFactory.class);
    
            return FXCollections.observableList(
                    loader.stream()
                            .map(
                                    ServiceLoader.Provider::get
                            ).sorted(
                                    Comparator.comparing(
                                            shapeFactory -> shapeFactory.getClass().getSimpleName()
                                    )
                            ).collect(
                                    Collectors.toList()
                            )
            );
        }
    
        private Pane createUI(ObservableList<ShapeFactory> shapeFactories) {
            ComboBox<ShapeFactory> shapeCombo = new ComboBox<>(shapeFactories);
            shapeCombo.setButtonCell(new ShapeFactoryCell());
            shapeCombo.setCellFactory(param -> new ShapeFactoryCell());
    
            StackPane shapeHolder = new StackPane();
            shapeHolder.setPrefSize(PREF_SHAPE_SIZE, PREF_SHAPE_SIZE);
    
            Button createShape = new Button("Create Shape");
            createShape.setOnAction(e -> {
                ShapeFactory currentShapeFactory = shapeCombo.getSelectionModel().getSelectedItem();
                Shape newShape = currentShapeFactory.createShape();
                shapeHolder.getChildren().setAll(newShape);
            });
            createShape.disableProperty().bind(
                    shapeCombo.getSelectionModel().selectedItemProperty().isNull()
            );
    
            HBox layout = new HBox(10, shapeCombo, createShape, shapeHolder);
            layout.setPadding(new Insets(10));
            layout.setAlignment(Pos.TOP_LEFT);
            return layout;
        }
    
        private static class ShapeFactoryCell extends ListCell<ShapeFactory> {
            @Override
            protected void updateItem(ShapeFactory item, boolean empty) {
                super.updateItem(item, empty);
    
                if (item != null && !empty) {
                    setText(item.getClass().getSimpleName());
                } else {
                    setText(null);
                }
            }
        }
    
        public static void main(String[] args) {
            launch();
        }
    }
    

    <?xml version="1.0" encoding="UTF-8"?>
    <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>
    
        <artifactId>shape-app</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>shape-app</name>
    
        <parent>
            <groupId>com.example</groupId>
            <artifactId>shapes</artifactId>
            <version>1.0-SNAPSHOT</version>
        </parent>
    
        <dependencies>
            <dependency>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-controls</artifactId>
                <version>${javafx.version}</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>shape-service</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>circle-provider</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>com.example</groupId>
                <artifactId>square-provider</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.openjfx</groupId>
                    <artifactId>javafx-maven-plugin</artifactId>
                    <version>0.0.8</version>
                    <executions>
                        <execution>
                            <!-- Default configuration for running with: mvn clean javafx:run -->
                            <id>default-cli</id>
                            <configuration>
                                <mainClass>com.example.shapeapp/com.example.shapeapp.ShapeApplication</mainClass>
                                <launcher>shape-app</launcher>
                                <jlinkZipName>shape-app</jlinkZipName>
                                <jlinkImageName>shape-app</jlinkImageName>
                                <noManPages>true</noManPages>
                                <stripDebug>true</stripDebug>
                                <noHeaderFiles>true</noHeaderFiles>
                                <bindServices>true</bindServices>
                                <runtimePathOption>MODULEPATH</runtimePathOption>
                                <jlinkVerbose>true</jlinkVerbose>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>
    

    Caveats