This library provides some utilities for Java SPI (Service Provider Interface).
The Java SPI is a mechanism that decouples a service from its implementation(s). It allows the creation of extensible or replaceable modules/plugins. It consists of four main components: a service, a service provider interface, some service providers and a service loader. If the service is a single interface then it is the same as a service provider interface.
Key points:
- lightweight library with no dependency
- no dependency at runtime, all the work is done at compile-time
- Java 8 minimum requirement
- has an automatic module name that makes it compatible with JPMS
[ Components | Design | Setup | Developing | Contributing | Licensing | Related work | Alternatives ]
Annotation | Purpose |
---|---|
@ServiceProvider | registers a service provider |
@ServiceDefinition | defines a service usage and generates a specialized loader |
@ServiceId | specifies the method used to identify a service provider |
@ServiceFilter | specifies the method used to filter a service provider |
@ServiceSorter | specifies the method used to sort a service provider |
The @ServiceProvider
annotation registers a service provider on classpath and modulepath.
Features:
- generates classpath files in
META-INF/services
folder - supports multiple registration of one class
- can infer the service if the provider implements/extends exactly one interface/class
- checks coherence between classpath and modulepath if
module-info.java
is available
Limitations:
- detects modulepath
public static provider()
method but doesn't generate a workaround for classpath
public interface FooSPI {}
public interface BarSPI {}
// π‘ One provider, one service
@ServiceProvider
public class FooProvider implements FooSPI {}
// π‘ One provider, multiple services
@ServiceProvider ( FooSPI.class )
@ServiceProvider ( BarSPI.class )
public class FooBarProvider implements FooSPI, BarSPI {}
The @ServiceDefinition
annotation defines a service usage and generates a specialized loader that enforces that specific usage.
Features:
- generates boilerplate code, thus reducing bugs and improving code coherence
- improves documentation by declaring services explicitly and generating javadoc
- checks coherence of service use in modules if
module-info.java
is available - allows identification
- allows filtering and sorting
- allows batch loading
- allows custom backend
Limitations:
- does not support type inspection before instantiation
Main properties:
#quantifier
: number of services expected at runtime#loaderName
: custom qualified name of the loader#fallback
: fallback type forSINGLE
quantifier#batchType
: bridge different services and generate providers on the fly
Advanced properties:
#mutability
: on-demand set and reload#singleton
: loader scope#wrapper
: wrapper type on backend#preprocessing
: custom operations on backend#backend
#cleaner
: custom service loader
The #quantifier
property specifies the number of services expected at runtime.
Values:
-
OPTIONAL
: when a service is not guaranteed to be available such as OS-specific API@ServiceDefinition(quantifier = Quantifier.OPTIONAL) public interface WinRegistry { String readString(int hkey, String key, String valueName); int HKEY_LOCAL_MACHINE = 0; static void main(String[] args) { // π‘ Service availability not guaranteed Optional<WinRegistry> optional = WinRegistryLoader.load(); optional.map(reg -> reg.readString( HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "ProductName")) .ifPresent(System.out::println); } }
-
SINGLE
: when exactly one service is guaranteed to be available@ServiceDefinition(quantifier = Quantifier.SINGLE, fallback = LoggerFinder.FallbackLogger.class) public interface LoggerFinder { Consumer<String> getLogger(String name); class FallbackLogger implements LoggerFinder { @Override public Consumer<String> getLogger(String name) { return msg -> System.out.printf(Locale.ROOT, "[%s] %s%n", name, msg); } } static void main(String[] args) { // π‘ Service availability guaranteed LoggerFinder single = LoggerFinderLoader.load(); single.getLogger("MyClass").accept("some message"); } }
-
MULTIPLE
: when several instances of a service could be used at the same time@ServiceDefinition(quantifier = Quantifier.MULTIPLE) public interface Translator { String translate(String text); static void main(String[] args) { // π‘ Multiple services expected List<Translator> multiple = TranslatorLoader.load(); multiple.stream() .map(translator -> translator.translate("hello")) .forEach(System.out::println); } }
The #loaderName
property specifies the custom qualified name of the loader.
// π‘ Name without interpretation
@ServiceDefinition(loaderName = "internal.FooSPILoader")
public interface FooSPI { }
An empty value generates an automatic name.
A non-empty value is interpreted as a Mustache template with the following tags:
Tag | Description |
---|---|
packageName |
The package name of the service class |
simpleName |
The service class name |
canonicalName |
The full service class name |
// π‘ Name with interpretation
@ServiceDefinition(loaderName = "internal.{{simpleName}}Loader")
public interface FooSPI { }
The #fallback
property specifies the fallback class to use if no service is available.
This property is only used in conjunction with Quantifier#SINGLE
.
@ServiceDefinition(quantifier = Quantifier.SINGLE, fallback = NoOpFooProvider.class)
public interface FooSPI { }
// π‘ Provider that does nothing except preventing NPE
public class NoOpFooProvider implements FooSPI { }
Note that a warning is raised at compile time if the fallback is missing
but this warning can be disabled with the @SupressWarning("SingleFallbackNotExpected")
annotation.
The #batchType
property allows to bridge different services and to generate providers on the fly.
Batch providers are used alongside regular providers.
@ServiceDefinition(quantifier = Quantifier.MULTIPLE, batchType = SwingColorScheme.Batch.class)
public interface SwingColorScheme {
List<Color> getColors();
static void main(String[] args) {
// π‘ Invisible use of RgbColorScheme
SwingColorSchemeLoader.load()
.stream()
.map(SwingColorScheme::getColors)
.forEach(System.out::println);
}
interface Batch {
Stream<SwingColorScheme> getProviders();
}
// π‘ Bridge between SwingColorScheme and RgbColorScheme
@ServiceProvider(Batch.class)
final class RgbBridge implements Batch {
@Override
public Stream<SwingColorScheme> getProviders() {
return RgbColorSchemeLoader.load()
.stream()
.map(RgbAdapter::new);
}
}
// π‘ Regular provider
@ServiceProvider(SwingColorScheme.class)
final class Cyan implements SwingColorScheme {
@Override
public List<Color> getColors() {
return Collections.singletonList(Color.CYAN);
}
}
}
Source: nbbrd/service/examples/SwingColorScheme.java
Constraints:
- Batch type must be an interface or an abstract class.
- Batch method must be unique.
The #mutability
property allows on-demand set and reload of a loader.
Example: nbbrd/service/examples/Messenger.java
The #singleton
property specifies the loader scope.
Example: nbbrd/service/examples/StatefulAlgorithm.java and nbbrd/service/examples/SystemSettings.java
The #wrapper
property allows service decoration before any map/filter/sort operation.
Example: TODO
The #preprocessor
property allows custom operations on backend before any map/filter/sort operation.
Example: TODO
The #backend
and #cleaner
properties allow to use a custom service loader such as NetBeans Lookup instead of JDK ServiceLoader
.
Example: nbbrd/service/examples/IconProvider.java
The @ServiceId
annotation specifies the method used to identify a service provider.
Properties:
#pattern
: specifies the regex pattern that the ID is expected to match
@ServiceDefinition(quantifier = Quantifier.MULTIPLE)
public interface HashAlgorithm {
// π‘ Enforce service naming
@ServiceId(pattern = ServiceId.SCREAMING_KEBAB_CASE)
String getName();
String hashToHex(byte[] input);
static void main(String[] args) {
// π‘ Retrieve service by name
HashAlgorithmLoader.load()
.stream()
.filter(algo -> algo.getName().equals("SHA-256"))
.findFirst()
.map(algo -> algo.hashToHex("hello".getBytes(UTF_8)))
.ifPresent(System.out::println);
}
}
Source: nbbrd/service/examples/HashAlgorithm.java
Characteristics:
- The
#pattern
property is used as a filter. - The
#pattern
property is available as a static field in the loader.
Constraints:
- It only applies to methods of a service.
- It does not apply to static methods.
- The annotated method must have no-args.
- The annotated method must return String.
- The annotated method must be unique.
- The annotated method must not throw checked exceptions.
- Its pattern must be valid.
The @ServiceFilter
annotation specifies the method used to filter a service provider.
Properties:
#position
: sets the filter ordering in case of multiple filters#negate
: applies a logical negation
@ServiceDefinition
public interface FileSearch {
List<File> searchByName(String name);
// π‘ General filter
@ServiceFilter(position = 1)
boolean isAvailableOnCurrentOS();
// π‘ Specific filter
@ServiceFilter(position = 2, negate = true)
boolean isDisabledBySystemProperty();
static void main(String[] args) {
FileSearchLoader.load()
.map(search -> search.searchByName(".xlsx"))
.orElseGet(Collections::emptyList)
.forEach(System.out::println);
}
}
Source: nbbrd/service/examples/FileSearch.java
Characteristics:
- There is no limit to the number of annotations per service.
- Filtering is done before sorting.
Constraints:
- It only applies to methods of a service.
- It does not apply to static methods.
- The annotated method must have no-args.
- The annotated method must return boolean.
- The annotated method must not throw checked exceptions.
The @ServiceSorter
annotation specifies the method used to sort a service provider.
Properties:
#position
: sets the sorter ordering in case of multiple sorters#reverse
: applies a reverse sorting
@ServiceDefinition
public interface LargeLanguageModel {
String summarize(String text);
// π‘ Maximize quality
@ServiceSorter(position = 1, reverse = true)
int getQuality();
// π‘ Minimize cost
@ServiceSorter(position = 2)
int getCost();
static void main(String[] args) {
LargeLanguageModelLoader.load()
.map(search -> search.summarize("bla bla bla"))
.ifPresent(System.out::println);
}
}
Source: nbbrd/service/examples/LargeLanguageModel.java
Characteristics:
- There is no limit to the number of annotations per service.
- Sorting is done after filtering.
Constraints:
- It only applies to methods of a service.
- It does not apply to static methods.
- The annotated method must have no-args.
- The annotated method must return double, int, long or comparable.
- The annotated method must not throw checked exceptions.
In some cases it is better to have a clear separation between API and SPI.
An API is designed to be called and used. It should be simple and foolproof.
An SPI is designed to be extended and implemented. It can be complex but should be performant.
Here is an example on how to do it:
public final class FileType {
// π‘ API: designed to be called and used
public static Optional<String> probeContentType(Path file) throws IOException {
for (FileTypeSpi probe : FileTypeSpiLoader.get()) {
String result = probe.getContentTypeOrNull(file);
if (result != null) return Optional.of(result);
}
return Optional.empty();
}
public static void main(String[] args) throws IOException {
for (String file : Arrays.asList("hello.csv", "stuff.txt")) {
System.out.println(file + ": " + FileType.probeContentType(Paths.get(file)).orElse("?"));
}
}
// π‘ SPI: designed to be extended and implemented
@ServiceDefinition(
quantifier = Quantifier.MULTIPLE,
loaderName = "internal.{{canonicalName}}Loader",
singleton = true
)
public interface FileTypeSpi {
enum Accuracy {HIGH, LOW}
String getContentTypeOrNull(Path file) throws IOException;
@ServiceSorter
Accuracy getAccuracy();
}
}
Source: nbbrd/service/examples/FileType.java
<dependencies>
<dependency>
<groupId>com.github.nbbrd.java-service-util</groupId>
<artifactId>java-service-annotation</artifactId>
<version>LATEST_VERSION</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.github.nbbrd.java-service-util</groupId>
<artifactId>java-service-processor</artifactId>
<version>LATEST_VERSION</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Alternate setup if the IDE doesn't detect the processor:
<dependencies>
<dependency>
<groupId>com.github.nbbrd.java-service-util</groupId>
<artifactId>java-service-processor</artifactId>
<version>LATEST_VERSION</version>
<scope>provided</scope>
</dependency>
</dependencies>
This project is written in Java and uses Apache Maven as a build tool.
It requires Java 8 as minimum version and all its dependencies are hosted on Maven Central.
The code can be build using any IDE or by just type-in the following commands in a terminal:
git clone https://github.com/nbbrd/java-service-util.git
cd java-service-util
mvn clean install
Any contribution is welcome and should be done through pull requests and/or issues.
The code of this project is licensed under the European Union Public Licence (EUPL).
This project is not the only one using with the SPI mechanism.
Here is a non-exhaustive list of related work:
The SPI mechanism is not suitable for all use cases. Here are some alternatives: