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"
}
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:
provides
and uses
statements for module-info.java
.ServiceLoader
javadoc, which should be thoroughly reviewed and understood.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:
ShapeFactory
interface that can create shapes.ShapeFactory
implementation that provides circles.ShapeFactory
implementation that provides squares.ShapeFactory
service providers and uses them to generate shapes.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
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.MODULEPATH
setting in the javafx-maven-plugin, it won't find your service modules.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.MANIFEST.MF
files). I recommend only supporting the 100% modular environment unless you absolutely also have to support classpath execution.