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.

View on GitHub Maven Central


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.load throws when several factories match, where findFirst() 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. disposeAll releases what it can and logs the rest.
  • Still ServiceLoader underneath. JPMS provides and uses, 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. Dependencies is built by the caller at the call site and handed to the factory; the only lookup is ServiceLoader’s own.
  • One limit, stated. The compiler checks what goes into Dependencies, not that the factory’s asks are met: a missing dependency fails at load(). 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.


Back to top

Copyright © 2026 druvu.com. All rights reserved.

This site uses Just the Docs, a documentation theme for Jekyll.