Home > Mobile >  ServiceLoader does not locate implementation when Gradle javafxplugin is used
ServiceLoader does not locate implementation when Gradle javafxplugin is used

Time:12-16

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

  • Splitting stuff up into modules and services like this makes things more complicated.
  • Many small things can go wrong, especially typos from copy and paste if you forget to change names.
  • The compiler won't pick up some typos because the service modules are dynamically discovered.
  • You can't mix package names across modules.
  • Linking with jlink is trickier because you need to ensure the services are on the jlinked module path and that you instruct jlink to bind them.
  • The tools are a bit obtuse in the error messages.
  • If not properly configured, the modules won't be found and the system will think that there are no matching services available.
  • Actually install all of your modules into your maven repository before each time you link it, otherwise it may pick up old versions or not find your software (this may not be actually necessary, but seemed the case in my experience).
  • Binding services will make your jlink image huge if you just use the default bind-services option. Apparently you can just bind listed services rather than all, but I could not find out how to do that with the javafx-maven-plugin. You can probably get more fine-grained control using the jlink command line than the maven plugin, though that would be more painful.
  • If you don't explicitly put a MODULEPATH setting in the javafx-maven-plugin, it won't find your service modules.
  • Always check the jlink verbose output to ensure that all of your expected services are being bound.
  • When working with a multi-module project like this, I strongly recommended keeping all of your modules at the same version number (centrally configured in the parent pom.xml), otherwise it is quite easy to link to obsolete versions. That isn't how this example project is setup though. To pin the version, define a shape.version property in the parent pom.xml and, wherever there is 1.0-SNAPSHOT, replace that with the ${shape-version}, then all projects will always use the same version.
  • The code assumes that everything is operating in a modular environment, with nothing running from the classpath. If you want to have services that work on the classpath, then you would need to do more work (e.g. to define service metadata in MANIFEST.MF files). I recommend only supporting the 100% modular environment unless you absolutely also have to support classpath execution.
  • I am sure there are more subtle and potential issues I haven't mentioned here.
  • Related