skip to content
everything.meta.serviceLoader

ServiceLoader

plugins without a framework.

chapter
chapter 11.08
reading time
8 min read

ServiceLoader finds implementations of an interface at runtime, including ones your code has never heard of. a provider jar announces itself with a file at META-INF/services/<interface name> on the class path, or with provides … with … in its module-info.java. the consumer just asks for the interface (and, in a module, says uses). providers are created lazily, one at a time, as you iterate.

that's how JDBC finds your database driver, javac finds annotation processors, and SLF4J finds its backend. no framework, it's been in the JDK since java 6.

the picture

you know how adding the postgres driver jar to your classpath makes DriverManager.getConnection("jdbc:postgresql://…") just work, even though nothing in your code mentions postgres? the driver jar registered itself.

think of a wall socket. the appliance (your code) is built against the socket's shape (an interface). whoever plugs in at the other end (a provider jar) supplies the power. the appliance never needs to know which power company it is, and you can switch companies by swapping a jar.

the three roles:

service
an interface (or abstract class) everyone agrees on. java.sql.Driver, javax.annotation.processing.Processor, your own com.shop.spi.Greeter.
provider
a class implementing it, in some jar, plus a declaration saying so.
consumer
the code that calls ServiceLoader.load(Greeter.class) and iterates whatever turns up.

hover a stage.

hover a stage. what one ServiceLoader.load call does on the class path. nothing happens until you start iterating, and each provider is created only when the iterator reaches it.

the smallest real one

the JDK uses it on itself. every command-line tool that can also run in-process registers as a java.util.spi.ToolProvider:

Main.java
import java.util.ServiceLoader;
import java.util.spi.ToolProvider;
 
void main() {
    var names = ServiceLoader.load(ToolProvider.class).stream()
        .map(provider -> provider.get().name())
        .sorted()
        .toList();
    IO.println(names);
 
    ToolProvider javac = ToolProvider.findFirst("javac").orElseThrow();
    IO.println(javac.getClass().getName());
}
output
[jar, javac, javadoc, javap, jdeps, jlink, jmod, jnativescan, jpackage]
com.sun.tools.javac.main.JavacToolProvider

nine tools, found at runtime, none of them named in your code. JavacToolProvider lives in the jdk.compiler module, in a package that module doesn't export: you couldn't write new JavacToolProvider() if you tried. you can still get one, because the module provides it.

a plugin, for real

an interface in one jar, two implementations in two more, and an app that only knows the interface:

com/shop/spi/Greeter.java (shop-api.jar)
package com.shop.spi;
 
public interface Greeter {
    String lang();
    String greet(String name);
}
com/shop/fr/French.java (greeter-fr.jar)
package com.shop.fr;
 
import com.shop.spi.Greeter;
 
public class French implements Greeter {
    public French() { System.out.println("  (French constructed)"); }
    public String lang() { return "fr"; }
    public String greet(String name) { return "bonjour, " + name; }
}
META-INF/services/com.shop.spi.Greeter (inside greeter-fr.jar)
com.shop.fr.French
Main.java (app.jar)
import com.shop.spi.Greeter;
import java.util.ServiceLoader;
 
public class Main {
    public static void main(String[] args) {
        for (Greeter g : ServiceLoader.load(Greeter.class)) {
            System.out.println(g.lang() + ": " + g.greet("ada"));
        }
    }
}

greeter-en.jar is the same with English. inside the provider jar, the registration is one small text file:

  • ├──
  • │ ├── MANIFEST.MF
  • │ └──
  • │ └── com.shop.spi.Greetercontains one line: com.shop.fr.French
  • └──
  • └──
  • └──
  • └── French.classimplements com.shop.spi.Greeter
what unzip -l greeter-fr.jar listed. the file's name is the service interface; its contents are the implementation class. that's the whole registration.

build them and change nothing but the class path:

terminal
$ java -cp shop-api.jar:app.jar Main
$ java -cp shop-api.jar:app.jar:greeter-en.jar Main
en: hello, ada
$ java -cp shop-api.jar:app.jar:greeter-en.jar:greeter-fr.jar Main
en: hello, ada
  (French constructed)
fr: bonjour, ada
$ java -cp shop-api.jar:app.jar:greeter-fr.jar:greeter-en.jar Main
  (French constructed)
fr: bonjour, ada
en: hello, ada

same app.jar, three behaviours. and look at where (French constructed) lands: after the English greeting, not at the start. the loader reads the service files up front but only instantiates each provider when the iterator reaches it.

run on =
options =
$ java -cp …
en: hello, ada
  (French constructed)
fr: bonjour, ada

ServiceLoader asks the class loader for every META-INF/services/com.shop.spi.Greeter file on the class path and reads the class names inside, in class path order. click a jar to remove it, or swap the order and watch which provider comes first (and when French gets constructed).

click the provider jars to take them off the path, swap their order, then switch to the module path and turn off uses. the blue text is each provider's declaration. every output is a real run.
quick check
greeter-en.jar declares com.shop.en.Engish in its services file (a typo). what happens?

the module way

on the module path, the text file is replaced by declarations the JVM checks when it resolves the module graph:

shop.fr/module-info.java
module shop.fr {
    requires shop.api;
    provides com.shop.spi.Greeter with com.shop.fr.French;
}
shop.app/module-info.java
module shop.app {
    requires shop.api;
    uses com.shop.spi.Greeter;
}

three things change:

  • the provider's package can stay private. shop.fr exports nothing. nobody can new French(), but ServiceLoader can still hand one out.
  • a provider can be a factory. if the class has a public static Greeter provider() method, that's called instead of a constructor, so the provider can return a singleton or a cached instance.
  • uses is mandatory. leave it out and using the loader throws (real message below). it's the module system's way of making service dependencies visible in the descriptor, where jlink can read them (jlink --bind-services pulls in every module that provides a service you use, see jlink).
text
Exception in thread "main" java.util.ServiceConfigurationError: com.shop.spi.Greeter: module shop.app does not declare `uses`

old-style jars on the class path still work when the app itself is a module: i put greeter-en.jar on the class path next to a modular shop.app, and the loader found it through its META-INF/services file. migrating is one side at a time (class path to module path).

where it's already working for you

serviceprovided bywhat it buys you
java.sql.Driverevery JDBC driver jarno more Class.forName("org.postgresql.Driver") since JDBC 4 (jdbc)
javax.annotation.processing.Processorlombok, mapstruct, daggerhow javac used to discover processors (annotation processing)
java.nio.file.spi.FileSystemProviderjdk.zipfs, your ownFileSystems.newFileSystem(zipPath) opens a zip as a file system
java.util.spi.ToolProviderthe JDK's toolsrun javac or jar in-process
System.LoggerFinderlogging backendswhere System.getLogger output goes (logging)
org.slf4j.spi.SLF4JServiceProviderlogback, log4j's slf4j bridgeSLF4J 2 picks its backend this way

compared to a DI container, it's tiny: no injection, no scopes, no configuration. just "give me every implementation of this interface that's on the path". that's often all a plugin system needs.

one level deeper:laziness, caching and threads+

each ServiceLoader instance caches the providers it has already created, and iterating it again replays the cache before looking for more. reload() clears that. two different ServiceLoader instances create two sets of provider objects, so providers are not singletons unless they make themselves one (with a provider() factory, for example). the loader itself isn't thread-safe; load once and share the instances you got. and stream() gives you Provider handles, so you can filter by type() and only call get(), which instantiates, on the one you want.

keylines
  • a service is an interface. providers declare themselves; the consumer just asks.
  • class path: a text file at META-INF/services/<interface>. module path: provides … with and uses.
  • providers are created lazily, in path order on the class path. a broken entry fails when it's reached.
  • a provider package doesn't need to be exported, and can use a static provider() factory.
  • shading jars? merge the service files, or your plugins vanish.