Skip to content

Components

A component is a plain class that weftkit's compile-time dependency injection constructs for you. @Wired marks a class as part of the dependency graph, and its constructor is the injection point. The annotations reference lists every annotation's exact contract.

Singletons and plain components

@Singleton creates a component once during load and injects that instance by type from then on. A plain @Wired component without @Singleton is created fresh for every injection. Use a singleton for anything that holds state or does startup work, and a plain component for throwaway, per-use objects.

@Wired
@Singleton
public final class SpawnerService {

    private final Config config;

    public SpawnerService(Config config) {
        this.config = config;
    }
}

Constructor injection

Every constructor parameter is resolved from the graph. A parameter can be another component, a value exposed with @Provides, or an ambient root. The plugin main is an ambient root because it carries @Registry, so any component can take it.

public SpawnerService(HelloPlugin plugin, Config config) { ... }

Binding to interfaces

A constructor can depend on an interface or an abstract class. When exactly one @Wired component implements it, the processor binds the two at compile time and that implementation is injected. For abstractions declared in your own sources, zero or several implementations fail the build, so the binding is never ambiguous at runtime. An abstraction from another module or jar resolves through a declared ambient type or a single wired implementation instead. When a singleton depends on one that has neither, or several implementations compete without a qualifier, the build fails too. Only a plain component may leave such a parameter open, to be filled by an explicit create argument.

public interface SpawnerStorage {
    void save(Spawner spawner);
}

@Wired
@Singleton
public final class SqlSpawnerStorage implements SpawnerStorage { ... }

@Wired
@Singleton
public final class SpawnerService {

    private final SpawnerStorage storage;

    public SpawnerService(SpawnerStorage storage) {
        this.storage = storage;
    }
}

Swapping the SQL implementation for a file based one is a one class change, and tests can construct SpawnerService with a fake directly since components are plain classes. get also resolves through bindings, so loader.get(SpawnerStorage.class) returns the bound singleton.

Products with @Provides

A no-argument getter on a singleton, annotated @Provides, exposes its return value to the graph once the owner has loaded. This is how you inject values you build at runtime rather than wire by type. The getter is public, or merely package-visible on a package-private owner.

@Wired
@Singleton
public final class Config implements Loader {

    private Greeting greeting;

    @Override
    public boolean load() {
        greeting = new Greeting("Hello");
        return true;
    }

    @Provides
    public Greeting greeting() {
        return greeting;
    }
}

Any component can now take a Greeting in its constructor.

weftkit captures the value once, right after the owner finishes loading. A getter that still returns null at that point fails startup, so a load that forgot to set its field is caught at enable time instead of at the first injection, and after shutdown the captured values are dropped again.

Reaching your components

WeftPlugin hands the WeftLoader to onWeftEnable, and loader() returns it anywhere in the plugin main while the plugin is enabled. Use it to reach singletons by type.

Config config = loader.get(Config.class);

For plain components, create builds a fresh instance, and createAll collects every component assignable to a type. Extra arguments are matched to constructor parameters by type, and among several assignable arguments the first given wins. Arguments never reach a singleton: create on a singleton rejects them outright, and createAll skips the singletons it fetches, since a cached instance must not depend on whichever call materialized it.

Report report = loader.create(Report.class);
List<Rule> rules = loader.createAll(Rule.class);

The loader itself is not injectable, and a WeftLoader constructor parameter fails the build. A component that needs to enumerate the graph injects Weft instead; everything else stays with the plugin main, which can hand the loader on after startup where a component genuinely needs it.

Optional dependencies

A parameter typed Optional<X> resolves to an empty Optional instead of failing when nothing provides X, including when a @Provides getter returns null. This is the natural shape for soft dependencies like a hook into another plugin that may not be installed.

public SellHandler(Optional<VaultHook> vault) { ... }

Qualifiers

When one type has several implementations or products, @Qualified tells them apart. On a @Wired class or a @Provides getter it tags what is offered, and on a constructor parameter it selects the matching tag. The processor checks every qualified dependency at compile time, so a missing or ambiguous tag fails the build.

@Wired
@Singleton
@Qualified("sql")
public final class SqlStorage implements SpawnerStorage { ... }

@Wired
@Singleton
@Qualified("file")
public final class FileStorage implements SpawnerStorage { ... }

public SpawnerService(@Qualified("sql") SpawnerStorage storage) { ... }

The same works for products, so one singleton can expose two values of the same type.

@Provides
public DataSource main() { ... }

@Provides
@Qualified("archive")
public DataSource archive() { ... }

A qualified parameter resolves only through its tagged implementation or product. Explicit arguments, ambient roots, and the loader itself carry no qualifier, so they never satisfy one.

@Qualified may also sit on a field, where the processor ignores it. That position exists for constructor generators: with Lombok, register the annotation as copyable and @RequiredArgsConstructor carries the tag onto the generated constructor parameter.

lombok.config
lombok.copyableAnnotations += org.weftkit.wiring.Qualified

Lazy singletons

@Singleton(lazy = true) defers creation to the first injection instead of building the component during load. Use it for expensive components that are rarely needed.

@Wired
@Singleton(lazy = true)
public final class BackupExporter { ... }

A lazy singleton takes part in the full lifecycle. A Loader implementation runs its load hook at materialization: a false return or exception drops the half-built singleton and propagates to the injection site, the rest of the graph stays loaded. The component then counts as failed: further injections rethrow the first failure cheaply instead of rebuilding a broken component over and over, until loader.resetFailure(...) or a full unload clears the marker. unload runs on shutdown in reverse creation order, interleaved with the eager singletons. A @Provides product materializes its lazy owner on demand, even when injected as an Optional. @Requires works as usual, and a lazy @Initializes initializer is materialized before any component that requires its holder.

Ambient roots

Ambient roots are values handed to the loader from outside the graph, available to every component by type. Their types are declared at compile time: the @Registry class itself, its constructor parameters, and the ambient attribute. The processor checks every dependency against the declarations, so an undeclared or ambiguously declared ambient dependency fails the build, and a value that matches no declared type is rejected when the plugin enables.

@Registry(ambient = Economy.class)
public final class HelloPlugin extends WeftPlugin { ... }

Values returned from ambientValues fill the declared types. (With manual control, they are the arguments to BukkitWeft.enable after the registry.) The plugin instance itself is always passed first, so JavaPlugin and your plugin main need no declaration.

@Override
protected Object[] ambientValues() {
    return new Object[] {economy};
}

A declared type without a value is simply absent: components depending on it as Optional<X> resolve empty. A FeatureFlags value may always be supplied without a declaration, see feature flags.