A small, runnable demo of addon-commons — a lightweight
Java library that turns the standard JDK ServiceLoader mechanism into a simple add-on / plugin system.
This project shows the full add-on lifecycle end to end:
- Define an add-on contract (an SPI interface).
- Provide several implementations.
- Register them as services.
- Discover and use them at runtime — both from the classpath and from external add-on jars.
The example domain is a playful Star Wars string converter: each add-on converts a name from one form
to another (e.g. "Anakin Skywalker" → "Darth Vader").
| Tool | Version |
|---|---|
| JDK | 11–21 (the build runs on these; compiled output targets Java 8 bytecode) |
| Gradle | Provided via the wrapper (./gradlew), Gradle 8.5 — no local install |
addon-commons |
16.0.0 |
Dependency note:
de.jensd:addon-commonsis not on Maven Central or JitPack. The build resolves it from your local Maven repository (~/.m2). If a clean build fails to resolve it, install/publishaddon-commonsto your local.m2first (clone addon-commons and run./gradlew publishToMavenLocal).
./gradlew build # compile + run all tests
./gradlew test # run tests only
./gradlew clean # remove build/
./gradlew publishToMavenLocal # publish this artifact (+ sources & javadoc jars) to ~/.m2Run a single test method:
./gradlew test --tests "de.jensd.addon.AddonRegistryTest.testConverter"There are three moving parts. Understanding how they fit together is the whole point of this demo.
StringConverter is the service interface. It extends the library's AddOn marker interface, which is
what makes it discoverable by the registry.
package de.jensd.addon.demo.converter;
import de.jensd.addon.AddOn;
public interface StringConverter extends AddOn {
String convert(String value);
String name(); // unique key used to look an add-on up
}Three converters implement the contract. Each returns a unique name() used as its lookup key:
| Implementation | name() |
Behaviour (example) |
|---|---|---|
DarkSideConverter |
DarkSideConverter |
"Anakin Skywalker" → "Darth Vader" |
LightSideConverter |
LightSideConverter |
"Darth Vader" → "Anakin Skywalker" |
CastConverter |
CastConverter |
"Luke" → "Mark Hamill" |
This is the step that is easy to forget. Implementations are registered with the JDK ServiceLoader via a
provider-configuration file named after the fully-qualified interface name:
src/main/resources/META-INF/services/de.jensd.addon.demo.converter.StringConverter
de.jensd.addon.demo.converter.DarkSideConverter
de.jensd.addon.demo.converter.LightSideConverter
de.jensd.addon.demo.converter.CastConverter
⚠️ An implementation that is not listed in this file will not be discovered. Adding a new converter always requires adding its fully-qualified class name here.
The library's AddOnRegistryServiceLoader finds and instantiates the registered add-ons. There are two
discovery modes:
a) From the classpath — picks up everything registered in META-INF/services:
AddOnRegistryServiceLoader registry = new AddOnRegistryServiceLoader();
List<StringConverter> converters = registry.getAddOns(StringConverter.class);
Map<String, StringConverter> byName = converters.stream()
.collect(Collectors.toMap(StringConverter::name, c -> c));
byName.get("DarkSideConverter").convert("Anakin Skywalker"); // -> "Darth Vader"b) From external add-on jars — scans a directory for add-on jars at runtime. The directory is configured
through a system property (its name is exposed as
AddOnRegistryServiceLoader.ADDON_LOOKUP_PATH_PROPERTY_NAME):
System.setProperty(AddOnRegistryServiceLoader.ADDON_LOOKUP_PATH_PROPERTY_NAME, "path/to/addons");
AddOnRegistryServiceLoader registry = new AddOnRegistryServiceLoader();
List<URL> addOnUrls = registry.lookupAddOnUrls();src/test/resources/addon-commons-demo-0.0.5.addon.jar is a pre-built add-on jar used to exercise this mode
in the tests (the project itself, packaged as an add-on).
- Create a class implementing
StringConverterundersrc/main/java/de/jensd/addon/demo/converter/. - Implement
convert(String)and return a unique value fromname(). - Append its fully-qualified class name to
src/main/resources/META-INF/services/de.jensd.addon.demo.converter.StringConverter. - Run
./gradlew test—AddonRegistryTest.testLoadExtensionsasserts the number of discovered converters, so update that assertion if you change the count.
src/
├── main/
│ ├── java/de/jensd/addon/demo/converter/
│ │ ├── StringConverter.java # the SPI contract (extends AddOn)
│ │ ├── DarkSideConverter.java
│ │ ├── LightSideConverter.java
│ │ └── CastConverter.java
│ └── resources/
│ ├── META-INF/services/
│ │ └── de.jensd.addon.demo.converter.StringConverter # service registration
│ └── log4j2.xml # console logging config
└── test/
├── java/de/jensd/addon/
│ └── AddonRegistryTest.java # end-to-end usage; best entry point
└── resources/
└── addon-commons-demo-0.0.5.addon.jar # sample add-on jar for jar-based discovery
AddonRegistryTest is the best place to start reading — it demonstrates both discovery modes and the
converter behaviour in one file.
- The project
versionis0.0.5.addon. The.addonsuffix is the add-on jar extension convention the library uses when discovering jar-based add-ons. - Every source file carries the Apache 2.0 license header — keep it on new files.
Licensed under the Apache License, Version 2.0. Copyright © Jens Deters — http://www.jensd.de