ServiceLoader discovers implementations of a service interface from provider configuration visible to a chosen class loader.
Java ServiceLoader: provider configuration and lazy discovery
This program targets Java 8 without preview flags. Use a full JDK for compiler-based examples.
Registration belongs to the provider package
A report exporter depends on an interface rather than a hard-coded implementation. On the class path, the provider lists its binary class name in META-INF/services followed by the interface binary name. A named module instead has uses and provides declarations; mixing the two layouts without checking deployment can produce an empty discovery result.
This fixture creates only the provider configuration in a temporary directory. The implementation class is already compiled with the program. A child URLClassLoader exposes the resource and delegates the implementation class to its parent, so the contract is tested without downloading or loading an unknown plugin.
Iteration can fail
Provider lookup and instantiation happen lazily. A configured class can be absent, incompatible, or unable to construct, and iteration can throw ServiceConfigurationError. Do not treat the first successfully returned provider as proof that the rest of the file is valid.
The fixture finds one exporter and calls its operation. Discovery does not register a priority or select the best provider for a workload. If selection depends on format, explicitly inspect a capability contract and reject ambiguous matches. ServiceLoader instances also need their own concurrency ownership; they are not general-purpose thread-safe registries.
Working program
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ServiceLoader;
public class ReceiptProviderDiscovery {
public interface Exporter { String format(); }
public static class TextExporter implements Exporter {
public TextExporter() {}
public String format() { return "receipt-text"; }
}
public static void main(String[] args) throws Exception {
Path root = Files.createTempDirectory("receipt-provider-");
Path services = Files.createDirectories(root.resolve("META-INF/services"));
Path config = services.resolve(Exporter.class.getName());
try {
Files.write(config, (TextExporter.class.getName()+"\n").getBytes(StandardCharsets.UTF_8));
try (URLClassLoader loader = new URLClassLoader(new URL[]{root.toUri().toURL()}, ReceiptProviderDiscovery.class.getClassLoader())) {
int count=0;
for (Exporter exporter : ServiceLoader.load(Exporter.class, loader)) {
System.out.println(exporter.format()); count++;
}
System.out.println("providers="+count);
}
} finally {
Files.deleteIfExists(config); Files.deleteIfExists(services);
Files.deleteIfExists(root.resolve("META-INF")); Files.deleteIfExists(root);
}
}
}Output
receipt-text
providers=1Costs and boundaries
Resource discovery and provider construction depend on the number of registrations and provider behavior. The program retains one provider and closes its own class loader. Closing a loader does not interrupt running plugin threads or provide a security boundary. Loading untrusted code requires a separate isolation design.
Common Mistakes
- Use binary names in class-path provider files, including a dollar sign for nested types.
- Do not assume provider iteration order expresses application priority.
- Do not suppress every ServiceConfigurationError and quietly run with a partial provider set.
