Package io.gdcc.spi.core.loader
Overview
This package is responsible for discovering, validating, and instantiating plugins
for a specific base plugin contract. The central entry point is PluginLoader,
which loads plugins from a filesystem location containing plugin artifacts such as JARs
or exploded directories.
The loader is intentionally validation-first:
- It discovers candidate plugin sources from a given location.
- It scans descriptor metadata from those sources.
- It validates the metadata before plugin classes are instantiated.
- It loads only the descriptors that passed validation, or those explicitly allowed by configuration as warning-level cases.
- It returns loaded plugins as
PluginHandleinstances, pairing the runtime plugin instance with its resolvedmetadata.
This design keeps failure handling predictable and avoids loading plugin classes unnecessarily when metadata already shows that a plugin is incompatible.
How the loader works
A PluginLoader is created for exactly one base plugin contract. That contract
must be a valid plugin interface intended to act as the loader's target type.
Calling PluginLoader.load(java.nio.file.Path) performs two broad phases:
1. Preload and validation
During preloading, the loader scans the configured sources and reads plugin descriptors. It then validates, among other things:
- class name collisions between plugins,
- class name collisions with classes already present in core,
- whether a descriptor matches the requested base contract,
- base contract API level compatibility,
- required provider compatibility, and
- identity uniqueness rules, depending on configuration.
Validation problems are represented as LoaderProblem instances. When the active
LoaderConfiguration is strict, incompatible plugins cause loading to abort with
a LoaderException. In more permissive configurations, some problems may be treated
as warnings and the loader may continue with the remaining valid plugins.
2. Class loading and instantiation
After successful prevalidation, plugin implementation classes are loaded from their
corresponding source locations and instantiated. Each successfully loaded plugin is returned
as a PluginHandle, which gives access both to the instantiated plugin and to the
resolved runtime descriptor.
Applying configuration options
Loader behavior is controlled through LoaderConfiguration. The configuration
type is immutable. The recommended style is to start from LoaderConfiguration.defaults()
and customize only the options that need to change.
Typical configuration concerns include:
- whether sources must contain only plugins matching the requested base contract,
- whether mixed sources should emit warnings,
- whether compatibility problems should abort loading, and
- whether ambiguous or duplicated plugin identities should be rejected.
Because configuration instances are immutable, they are easy to reuse, share, and test.
Example: customizing loader behavior
LoaderConfiguration configuration = LoaderConfiguration.defaults()
.withEnforceSingleSourceMatchingPluginsOnly(false)
.withEmitWarningsOnMultiPluginSource(true)
.withAbortOnCompatibilityProblems(false);
PluginLoader<MyPluginContract> loader =
new PluginLoader<>(MyPluginContract.class, configuration);
What to expect
- Early validation: many incompatibilities are detected from metadata before plugin classes are instantiated.
- Aggregated diagnostics: failures are reported as collections of
problemsthroughLoaderException.getProblems(). - Contract-focused loading: each loader instance targets one base plugin contract at a time.
- Structured runtime results: successful loads are represented by
PluginHandlevalues rather than raw plugin instances alone. - Configurable strictness: callers can choose between fail-fast and more permissive behavior depending on operational needs.
What not to expect
- No hot reload: this package supports loading, not live reloading of changed plugin artifacts. Updated plugins generally require a fresh loading cycle and typically an application restart strategy at a higher level.
- No legacy-plugin compatibility guarantee: the loader operates on the descriptor-based plugin model and does not aim to support older plugins that do not participate in that model.
- No cross-loader identity coordination: identity uniqueness is checked within the scope of a loader run, not globally across every possible plugin source in an application.
- No substitute for packaging discipline: plugin authors are still expected to provide correct metadata, compatible API levels, and non-conflicting classes and identities.
Usage examples
Example 1: Loading plugins with default settings
try {
PluginLoader<MyPluginContract> loader = new PluginLoader<>(MyPluginContract.class);
List<PluginHandle<MyPluginContract>> plugins = loader.load(Path.of("plugins"));
// Use or store plugins.
} catch (LoaderException ex) {
for (LoaderProblem problem : ex.getProblems()) {
System.err.println(problem.message());
}
} catch (IllegalArgumentException ex) {
System.err.println(ex.getMessage());
}
Example 2: Using an explicit parent class loader
ClassLoader parent = Thread.currentThread().getContextClassLoader();
PluginLoader<MyPluginContract> loader = new PluginLoader<>(MyPluginContract.class, parent);
In most applications, callers only need PluginLoader and
LoaderConfiguration. The remaining types in this package primarily support
diagnostics, runtime result transport, and internal validation mechanics.
-
ClassDescriptionImmutable configuration controlling the behavior of the plugin loader.PluginHandle<T extends Plugin>Encapsulates a plugin and its corresponding descriptor, providing a unified representation of a resolved plugin and its metadata in the runtime context.PluginLoader<T extends Plugin>Loads plugins of a specified type from JAR files in a given directory using the Java ServiceLoader mechanism.