🔴 The Error You're Seeing
Confirm this matches your console output. If it does, you're in the right place.
// DOC-DERIVED — wording stable since JDK 9; JDK 8 used "Provider ... not found":
Exception in thread "main" java.util.ServiceConfigurationError: com.devinhyderabad.export.Exporter: provider com.devinhyderabad.export.PdfExporter not found
at java.base/java.util.ServiceLoader.fail(ServiceLoader.java:586)
at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.nextProviderClass(ServiceLoader.java:1160)
at com.devinhyderabad.app.ReportRunner.main(ReportRunner.java:12)⚡ Quick Fix Works 80% of the time
Ensure the jar that DEFINES PdfExporter ships alongside the jar declaring the META-INF/services entry — in fat jars verify both class file and service file made it into the archive.
unzip -l app.jar | grep -E "PdfExporter.class|META-INF/services/com.devinhyderabad.export.Exporter"
# BOTH lines must appear - missing the first = this exact error🧠 Why this Happens
Tap to expand the deep technical explanation
ServiceLoader resolves an SPI in two independent steps. It first reads META-INF/services/<interface-FQCN>, a plain text file listing implementation class names — this succeeds because the text resource traveled with your API jar. Only when iteration reaches the entry does it attempt Class.forName on the listed name; if the implementing class’s bytecode never shipped (separate artifact excluded from the fat jar, module path missing its provides directive, or a rename outliving the config file), loading fails and ServiceLoader wraps the failure in ServiceConfigurationError at next() rather than at startup.
The HITEC City Parking Spot Analogy:
A restaurant menu lists "Chef’s Special" but the kitchen never hired that chef. Ordering goes fine until the plate must actually be cooked — then staff admit nobody back there can make it.
🔁 How to Reproduce Confirm this is your error
Create an SPI interface plus META-INF/services file naming an implementation, ship the interface jar WITHOUT the impl class, iterate ServiceLoader.load(Exporter.class).next(). DOC-DERIVED — deterministic lazy failure.
🛠️ Solutions (5 Ways to Fix)
Ship provider and declaration as one unit
👉 Use this whenever you control packaging — the simplest permanent cure.
Bundle the implementing class in the same artifact (or a guaranteed companion) as its services file. Shade plugins merge META-INF/services via ServiceResourceTransformer so aggregation cannot orphan either half.
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<!-- merges META-INF/services across all shaded jars -->Audit the deployed archive for both halves
👉 Use this during diagnosis on any jar/war/docker image already built.
One grep proves which half went missing: the services text file, the provider .class, or both. Then trace why packaging dropped it — exclusion filters, minimizeJar stripping, Docker COPY globs.
unzip -l app.jar | grep -E "PdfExporter|services/.*Exporter"
# shade minimizeJar often strips providers referenced only reflectively:
<minimizeJar>false</minimizeJar>Declare provides directives on the module path
👉 Use this for JPMS deployments where META-INF alone no longer registers providers.
In named modules ServiceLoader consults module declarations; a module exporting the SPI but never providing implementations yields exactly this error at iteration. Add provides...with to the implementing module.
module devtoolpick.export.pdf {
requires devtoolpick.export.api;
provides com.devinhyderabad.export.Exporter
with com.devinhyderabad.export.PdfExporter;
}Keep the services file synchronized with refactoring
👉 Use this after package renames or class moves inside the provider project.
The text file names classes verbatim; IDE refactors never touch it. Add a unit test that loads every declared provider and fails on drift, converting silent runtime rot into build-time failure.
@Test void everyDeclaredProviderLoads() {
assertDoesNotThrow(() ->
ServiceLoader.load(Exporter.class).forEach(p -> assertNotNull(p)));
}Catch the error and continue with remaining providers
👉 DEV ONLY — masks wiring bugs; see note.
DEV ONLY. Iterating with try/catch around next() lets partial functionality survive a broken provider, but it hides packaging defects until users notice missing features. Reserve for plugin hosts that genuinely tolerate absent extensions — and log loudly.
// DEV ONLY - tolerant plugin host pattern
Iterator<Exporter> it = ServiceLoader.load(Exporter.class).iterator();
while (it.hasNext()) {
try { register(it.next()); }
catch (ServiceConfigurationError e) { e.printStackTrace(); /* keep going */ }
}📋 Version Notes
Message reads Provider com.x.Y not found; same lazy-failure semantics.
Wording lowercase since 9; module-path resolution added.
Unchanged; stream() API makes diagnostics easier.
Unchanged.
🛡️ How to Prevent This Next Time
Test SPI loading in CI against the REAL packaged artifact (not exploded classes), merge service files correctly when shading, and declare provides/uses explicitly once any part of the deployment moves onto the module path.