druvu-lib-loader
ServiceLoader finds an implementation but cannot pass it a constructor argument. This is the factory that does, written once. Discovery was never the problem. Handing the implementation a Path, a Config, a DataSource was. Here the caller hands over typed Dependencies, the factory receives them, and the component is created valid, with no init() phase.
If your implementations are no-arg and unique, plain ServiceLoader already serves you. If a container such as Spring or Guice owns construction, keep it. This is for libraries and plugins that should not impose one.
Try it
Give the interface a static load(...), so callers need only one type:
package com.myapp.api;
import java.nio.file.Path;
import java.util.List;
import com.druvu.lib.loader.ComponentLoader;
import com.druvu.lib.loader.Dependencies;
public interface AccBook {
String id();
List<String> accountNames();
// Convenience factory — discovers the implementation via ServiceLoader
static AccBook load(Path path) {
return ComponentLoader.load(AccBook.class, Dependencies.of(Path.class, path));
}
}
In the implementation module, a factory. GnucashAccBook is your implementation of AccBook; its constructor takes the Path:
package com.myapp.gnucash.io;
import java.nio.file.Path;
import com.druvu.lib.loader.ComponentFactory;
import com.druvu.lib.loader.Dependencies;
import com.myapp.api.AccBook;
public class GnucashBookFactory implements ComponentFactory<AccBook> {
@Override
public AccBook createComponent(Dependencies dependencies) {
var path = dependencies.getDependency(Path.class);
return new GnucashAccBook(path);
}
@Override
public Class<AccBook> type() {
return AccBook.class;
}
}
Register it in META-INF/services/com.druvu.lib.loader.ComponentFactory, or in module-info.java:
provides com.druvu.lib.loader.ComponentFactory
with com.myapp.gnucash.io.GnucashBookFactory;
Then:
AccBook book = AccBook.load(Path.of("/path/to/file.xml"));
Your application code depends on the API module only.
What you get
- Dependencies at creation. The factory receives type-keyed
Dependencies;Dependencies.of(Path.class, path, Config.class, config)for several. - Fail-fast cardinality.
ComponentLoader.loadthrows when several factories match, wherefindFirst()would silently pick one, and throws when none do. - The plugin case.
MultiComponentLoader.loadAll(Plugin.class, deps)returns every implementation, each created with the same dependencies.disposeAllreleases what it can and logs the rest. - Still
ServiceLoaderunderneath. JPMSprovidesanduses, no reflection scanning, no annotations, no container. An implementation with a no-arg constructor is found as a fallback without depending on this library at all. - Nothing global.
Dependenciesis built by the caller at the call site and handed to the factory; the only lookup isServiceLoader’s own. - One limit, stated. The compiler checks what goes into
Dependencies, not that the factory’s asks are met: a missing dependency fails atload(). Call it at startup and you find out at startup.
Install
On Maven Central. Java 21 or newer. No runtime dependencies since 1.1.2; the few warnings it logs go through the JDK’s System.Logger.
<dependency>
<groupId>com.druvu</groupId>
<artifactId>druvu-lib-loader</artifactId>
<version>1.1.2</version>
</dependency>
implementation 'com.druvu:druvu-lib-loader:1.1.2'
With the Java module system: requires com.druvu.lib.loader; Before 1.1.0 the module was named druvu.lib.loader.
The full reference, every loader and the no-library fallback, is the README.
Something it almost does?
Open an issue, or write to me. If it is small and useful to more than you, I will often just build it.
Apache License 2.0.