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.
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.
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.
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.
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.
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.
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.
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.
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.
Related¶
- Annotations reference: the exact contract of
@Wired,@Singleton, and@Provides - Internal components: wiring package-private classes
- Testing: constructing components with fakes, no framework involved
- Plugin lifecycle: when components load and shut down