Java SPI
Java SPI (Service Provider Interface) is the JDK’s built-in plugin discovery mechanism. It lets a library define an interface and have implementations discovered at runtime without compile-time coupling — the library never imports the implementation class.
How it works
- A library publishes an interface (the “service”):
package com.example;
public interface MyService {
String name();
void doWork();
}- An implementation ships a descriptor file in its JAR at a well-known path:
META-INF/services/com.example.MyService
The file contains the fully-qualified class name of the implementation (one per line):
com.provider.MyServiceImpl
Each JAR that wants to provide an implementation ships its own copy of this file. Multiple JARs can all provide a file with the same name (META-INF/services/com.example.MyService), each listing their own implementation class.
- At runtime, the library discovers all registered implementations via
ServiceLoader:
ServiceLoader<MyService> loader = ServiceLoader.load(MyService.class);
for (MyService impl : loader) {
System.out.println(impl.name());
}ServiceLoader does not scan the classpath for classes implementing the interface — that would require loading every class, which is prohibitively slow. Instead, it reads all files named META-INF/services/com.example.MyService across every JAR on the classpath, concatenates their contents, and instantiates each listed class via its no-arg constructor. The descriptor file is the registration — without it, an implementation is invisible to ServiceLoader.
For example, Spark discovers DataSource V2 providers this way:
spark-core.jar
└── META-INF/services/o.a.s.sql.sources.DataSourceRegister
→ org.apache.spark.sql.parquet.ParquetDataSource
indextables.jar
└── META-INF/services/o.a.s.sql.sources.DataSourceRegister
→ io.indextables.spark.IndexTables4SparkTableProvider
delta-lake.jar
└── META-INF/services/o.a.s.sql.sources.DataSourceRegister
→ org.apache.spark.sql.delta.DeltaDataSource
ServiceLoader.load(DataSourceRegister.class) finds all three descriptor files and instantiates all three providers.
Why it matters
SPI decouples discovery from usage. The library that defines the interface does not need to know which implementations exist — they register themselves by placing a file in the right location. This is how:
- JDBC drivers register themselves (
java.sql.Driver) - Logging frameworks discover backends (
org.slf4j.spi.SLF4JServiceProvider) - Apache Spark discovers DataSource V2 providers (
org.apache.spark.sql.sources.DataSourceRegister) — see TableProvider (format-based access) - Java’s
Charset,FileSystem, andCurrencyProviderare all SPI-based
Limitations
- Discovery requires reading
META-INF/services/from every JAR on the classpath (I/O-heavy on large classpaths with hundreds of JARs, though results are cached after first load) - Implementations must have a public no-arg constructor
- No dependency injection, no ordering guarantees, no conditional registration
- Module system (Java 9+) adds
provides ... with ...declarations inmodule-info.javaas a compile-time alternative to theMETA-INFfile
See also
- Spark SQL Extension Framework — uses SPI for DataSource V2 TableProvider registration