Package io.gdcc.spi.core.loader


package io.gdcc.spi.core.loader
Provides the runtime plugin loading facilities for the SPI-based plugin system.

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:

  1. It discovers candidate plugin sources from a given location.
  2. It scans descriptor metadata from those sources.
  3. It validates the metadata before plugin classes are instantiated.
  4. It loads only the descriptors that passed validation, or those explicitly allowed by configuration as warning-level cases.
  5. It returns loaded plugins as PluginHandle instances, pairing the runtime plugin instance with its resolved metadata.

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 problems through LoaderException.getProblems().
  • Contract-focused loading: each loader instance targets one base plugin contract at a time.
  • Structured runtime results: successful loads are represented by PluginHandle values 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.