🪟 Windows TippsModify Windows Support Phone Number with PowerShell(03.09.2026 um 00:00 Uhr)
🔧 AI Nachrichten Podcast: ChatGPT schwatzt Nutzern in Deutschland jetzt Werbung auf(28.08.2026 um 08:46 Uhr)
🪟 Windows TippsMicrosoft bringt Emoji 17.0 auf Windows 11(31.08.2026 um 08:16 Uhr)
🪟 Windows TippsModify Windows Support Phone Number with PowerShell(03.09.2026 um 00:00 Uhr)
🔧 AI Nachrichten Podcast: ChatGPT schwatzt Nutzern in Deutschland jetzt Werbung auf(28.08.2026 um 08:46 Uhr)
🪟 Windows TippsMicrosoft bringt Emoji 17.0 auf Windows 11(31.08.2026 um 08:16 Uhr)

🔧 Programmierung 🕛 vor 2 Monaten 5 Min Lesezeit
0

ServiceLoader is great — until you want a constructor argument

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

I was building a small library that reads accounting files — GnuCash first, other formats maybe later. It's the textbook case for pluggable implementations: an AccBook interface in the API module, one implementation per format in its own module, and the application picks up whichever one is on the path.



Why not AccBook book = new GnucashAccBook(path) and move on? For one format in one app — do exactly that. But the moment your code says new GnucashAccBook, it depends on that implementation: the GnuCash module has to be on the compile path, its classes exported, its name written in every place that opens a book. Add a second format and each of those places grows an if. The interface stops protecting you once you name the class behind it.



Java already ships the tool for exactly this. No framework, no container, works on the module path: ServiceLoader. So I reached for it. Then I hit a wall I'd hit before and forgotten about.






The naive version



The interface is plain:




CODE
public interface AccBook {
String id();
List<Account> accounts();
}






Discovery is genuinely easy:




CODE
AccBook book = ServiceLoader.load(AccBook.class).findFirst().orElseThrow();






But my GnucashAccBook can't do anything without the file path. And ServiceLoader only knows how to call a no-arg constructor (or a static provider() method). There is nowhere to hand it the Path.



So you reach for one of two workarounds, and both are worse than they look.



Workaround 1 — construct empty, configure after:




CODE
AccBook book = ServiceLoader.load(AccBook.class).findFirst().orElseThrow();
((Configurable) book).init(path); // ...and hope nobody touched `book` before this line






Now the object exists in an invalid state between construction and init. You've swapped a constructor argument for a runtime contract the compiler can't see. (And findFirst() quietly picks one if two implementations are on the path — hold that thought.)



Workaround 2 — put a factory behind ServiceLoader:




CODE
public interface AccBookFactory {
AccBook create(Path path);
}

AccBook book = ServiceLoader.load(AccBookFactory.class)
.findFirst().orElseThrow()
.create(path);






This one is actually the right shape. But now every project grows an XFactory interface, the findFirst().orElseThrow() dance, and — if you care that two implementations on the path is a mistake — your own "throw if more than one" check. I wrote that glue three times before I admitted it was a pattern.






Extracting the pattern



So I pulled it into a tiny library, druvu-lib-loader. It is the factory-behind-ServiceLoader pattern, made generic once, with the implementation's arguments passed through a type-keyed map.



The implementation side becomes a factory that receives its arguments:




CODE
public class GnucashBookFactory implements ComponentFactory<AccBook> {
@Override
public AccBook createComponent(Dependencies deps) {
return new GnucashAccBook(deps.getDependency(Path.class));
}
@Override
public Class<AccBook> type() {
// Seasoned Spring developers will recognize the silhouette of an old friend.
// This one retires right after construction, though — no container moves in.
return AccBook.class;
}
}






You register it exactly like any other service — META-INF/services, or in module-info.java:




CODE
provides com.druvu.lib.loader.ComponentFactory
with com.myapp.gnucash.GnucashBookFactory;






And in the API module you give callers one obvious entry point — a static load right on the interface:




CODE
public interface AccBook {
String id();
List<Account> accounts();

static AccBook load(Path path) {
return ComponentLoader.load(AccBook.class, Dependencies.of(Path.class, path));
}
}






So everything the caller ever sees is this:




CODE
AccBook book = AccBook.load(path);






The constructor argument is passed. A single implementation is guaranteed — ComponentLoader.load throws if two factories match the type, instead of silently choosing one. No container, no annotations, no classpath scanning.



And when multiple implementations are the point — a plugin system, an exporter per format — the same pattern flips around:




CODE
List<Exporter> exporters = MultiComponentLoader.loadAll(Exporter.class, deps);






Every registered factory gets the same Dependencies, you get all the instances. Same discovery, same argument-passing, opposite cardinality rule.






It's not magic



AccBook.load(path) isn't reflection sorcery and it isn't a new DI framework. Underneath it is still ServiceLoader, still the JPMS provides/uses you would have written anyway. The static load is a plain method you write once. The only thing the library adds is the part the JDK left out: a typed way to pass arguments to the implementation, plus a fail-fast "exactly one" rule.



One honest caveat. The compiler checks what you put into DependenciesDependencies.of(Path.class, path) won't accept a String where a Path belongs. But it can't check that you supplied everything the factory will ask for. Forget the Path and the code still compiles — it fails only when load() runs and getDependency(Path.class) comes up empty. This is a real difference from Spring: Spring checks all its wiring at startup, so a missing bean stops the app from booting. Here there is no startup check — the check happens when you call load(). Call it at startup and you catch the mistake at startup. Call it later, and you catch it later. The good news: the load(...) call and the factory that reads it sit right next to each other, so when it fails, there is one obvious place to look. But the compiler won't catch it for you.






When not to use it



If your implementations have no-arg constructors, you don't need any of this — plain ServiceLoader is the right answer, and the library falls back to it anyway. If you already run inside Spring or Guice, use the container you have; this isn't trying to replace it. It earns its place in one specific spot: libraries and modular components that need pluggable, argument-taking implementations without dragging in a container.






That's the whole idea



It's on Maven Central as com.druvu:druvu-lib-loader — source and docs on and disagreement welcome.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
1 Quelle
Modify Windows Support Phone Number with PowerShell
1 Quelle
Die Zukunft des Einkaufens: Warum wir ein neues Kapitel aufschlagen (und wie du es mitschreiben kannst)
1 Quelle
ZDE Podcast 251: Wie sieht digitales Instore Marketing 2026 aus, Amit Chatterjee?
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten ServiceLoader is great — until you want a constructor argument

Thematisch verwandte Begriffe: ServiceLoader, great, until, want · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...