Skip to content

Add scoped plugin service discovery API - #13625

Open
Hiteshsai007 wants to merge 7 commits into
apache:masterfrom
Hiteshsai007:codex/issue-13516
Open

Hiteshsai007 wants to merge 7 commits into
apache:masterfrom
Hiteshsai007:codex/issue-13516

Conversation

@Hiteshsai007

@Hiteshsai007 Hiteshsai007 commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Resolves #13516

Build extensions that inspect project plugins and discover plugin-provided services currently need to use internal Maven APIs. This requires combining plugin resolution, realm creation, Plexus component lookup, and manual thread context classloader management.

This change introduces a public Maven 4 service for loading a project plugin and discovering components within that plugin's classloader scope.

How this solves the problem

The new PluginManager API provides two operations:

  1. Load a project plugin

    loadPlugin(...) resolves missing plugin versions, loads the plugin descriptor, creates the plugin realm when necessary, and returns the public Plugin runtime representation.

  2. Expose plugin runtime metadata

    The returned Plugin provides access to its model, descriptor, artifact, classloader, lifecycle mappings, and resolved dependencies without requiring consumers to access the internal MavenPluginManager.

  3. Perform classloader-scoped service discovery

    lookupList(...) performs component discovery with the selected plugin's classloader as the thread context classloader.

  4. Restore the original classloader

    The original thread context classloader is restored after every lookup, including when component provisioning fails.

The existing plugin snapshot construction from DefaultMojoExecution is also moved into DefaultPlugin, ensuring that mojo executions and the new service expose consistent plugin metadata.

Why this matters

Build extensions such as Tycho need to inspect project plugin dependencies and discover SPI implementations contributed by individual plugins.

Previously, this required using internal LifecyclePluginResolver, MavenPluginManager, BuildPluginManager, and PlexusContainer APIs while manually managing the thread context classloader. These internal dependencies make extensions more tightly coupled to Maven implementation details and harder to migrate to the Maven 4 API.

By keeping plugin resolution, realm creation, and scoped component lookup inside Maven, extensions can implement this behavior using stable Maven 4 API types.

Testing

Added DefaultPluginManagerTest coverage verifying:

  • Missing plugin versions are resolved before loading the plugin.
  • Plugin descriptors, classloaders, and resolved dependencies are exposed through the public Plugin API.
  • The plugin realm is created during loading.
  • Components are discovered using the plugin classloader.
  • Lookup results are immutable.
  • The original thread context classloader is restored after successful and failed lookups.

The focused maven-core reactor tests for DefaultPluginManagerTest and DefaultMojoExecutionTest pass successfully with 6 tests and 0 failures.

Checklist

  • Your pull request should address just one issue, without pulling in other changes.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied.
  • Run mvn verify to make sure basic checks pass.
  • You have run the Core IT successfully.

To make clear that you license your contribution under the Apache License Version 2.0, January 2004, you have to acknowledge this by using the following checkbox.

Expose a Maven 4 PluginManager service that resolves declared project plugins into public runtime metadata and performs component lookups with the plugin realm as the context classloader.

This removes the need for build extensions to combine internal plugin manager and container APIs for plugin-contributed services.
@gnodet

gnodet commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

We've updated the classloader improvements tracking issue (#13223) to include a Layer 0b — Public Realm API that aligns with the direction of this PR but proposes a slightly different shape.

The idea: instead of exposing Plugin.getClassLoader() and having PluginManager.lookupList() do manual TCCL swapping, introduce a PluginRealm concept that bundles a classloader with a scoped Lookup:

public interface PluginRealm {
    ClassLoader getClassLoader();
    Optional<ModuleLayer> getModuleLayer(); // for future JPMS ModuleLayer support
    Lookup getLookup(); // scoped to this realm's import chain
}

Plugin.getRealm() replaces Plugin.getClassLoader(), and callers do plugin.getRealm().getLookup().lookupList(type) — no manual Thread.setContextClassLoader() needed.

Benefits:

  • Encapsulates TCCL handling inside the Lookup implementation
  • Abstracts classpath vs JPMS — same API for classpath-based and ModuleLayer-based realms
  • Enforces visibility — the Lookup is pre-scoped, so component visibility follows the realm's import chain by construction
  • Removes PluginManager.lookupList() — the realm owns its scoped lookup directly

See #13223 for the full plan, specifically the "Layer 0b" and "Design Principle — Component Visibility Follows Classloading Rules" sections.

Claude Code on behalf of Guillaume Nodet

Replace direct plugin classloader access with a PluginRealm that exposes the classloader, an optional module layer, and a realm-scoped Lookup.

Keep TCCL management inside the Lookup implementation so callers cannot accidentally perform an unscoped component lookup.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall the design is clean: PluginRealm as a rich API type over a bare ClassLoader is the right direction, RealmLookup.scoped() correctly restores the TCCL, and the test coverage for TCCL restore and exception propagation is solid.

A few things need to be addressed before merge.

Binary-breaking API change — Plugin.getClassLoader() was removed and replaced by Plugin.getRealm(). Any embedder or plugin already calling plugin.getClassLoader() gets a compile error. This is intentional (4.1.0 is still in development), but please confirm it is tracked, and add a note in the PR description or a BREAKING CHANGES entry.

Accessor naming convention — PluginRealm uses getClassLoader(), getModuleLayer(), getLookup(). Maven 4 API prefers noun-style accessors (classLoader(), moduleLayer(), lookup()). See e.g. Project, Session, MojoExecution. Worth aligning before stabilizing this API.

Nullable model field — DefaultPlugin.model can be null (when the model parameter is null). Plugin.getModel() is already @Nullable, so no contract violation, but a @Nullable annotation on the constructor parameter would make the intent explicit.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Comment thread api/maven-api-core/src/main/java/org/apache/maven/api/Plugin.java Outdated
Comment thread api/maven-api-core/src/main/java/org/apache/maven/api/PluginRealm.java Outdated
Use Maven 4 noun-style realm accessors, document the new API, and preserve undeclared-plugin errors. Add the requested exception constructors and serialization metadata, nullable annotation, and regression coverage.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier review — all four previous findings have been addressed:

  • ✅ Plugin.getRealm() now has @since 4.1.0
  • ✅ PluginRealm methods all have @return tags
  • ✅ PluginManagerException has @Serial serialVersionUID and both constructors
  • ✅ findPlugin() is now called outside the try block — IllegalArgumentException propagates correctly

One new gap: PluginManager.loadPlugin() throws IllegalArgumentException (from findPlugin() when the plugin is not declared by the project) but this is not documented in the @throws section. Callers relying on the API contract have no signal to expect or catch IAE — they’ll see it as an undeclared runtime exception with no documentation on the condition that triggers it.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Declare the IllegalArgumentException thrown by PluginManager.loadPlugin when the requested plugin is not present in the project build.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All five findings from the prior reviews are now addressed. One new documentation-accuracy issue remains.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Comment thread api/maven-api-core/src/main/java/org/apache/maven/api/Realm.java Outdated
Generalize the interface for use at all levels (session, project,
plugin). The same Realm shape — classLoader(), moduleLayer(), lookup()
— applies uniformly; the difference is what the Lookup sees, not the
type. Module layers are not plugin-specific (extensions and core can
also use them, e.g. JLine per apache#13491).

See apache#13223 (Layer 0b) for the design rationale.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier review — the @throws IllegalArgumentException is now documented on PluginManager.loadPlugin() (thread 1 addressed). Thread 2 (Realm.lookup() Javadoc accuracy) remains open.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Comment thread api/maven-api-core/src/main/java/org/apache/maven/api/Realm.java
Document that realm lookup delegates with the realm classloader as the TCCL and that component visibility depends on container filtering.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier reviews — all previous findings are now resolved:

  • ✅ Plugin.getClassLoader() → Plugin.getRealm() binary break: intentional for 4.1.0, documented
  • ✅ Accessor naming: Realm uses noun-style (classLoader(), moduleLayer(), lookup())
  • ✅ @throws IllegalArgumentException documented on PluginManager.loadPlugin()
  • ✅ Realm.lookup() Javadoc: softened to accurately describe TCCL-switching semantics

The API is clean and the implementation is correct. LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Use the current context classloader, with the Maven implementation classloader as a final fallback, when legacy descriptors have no realm attached. Restore regression coverage for minimal mojo descriptors used by expression evaluation.

@gnodet-bot gnodet-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my earlier reviews — the new commit (734bfed) is clean.

All prior findings remain addressed:

  • ✅ Plugin.getRealm() replaces getClassLoader() — breaking change intentional for 4.1.0, documented with @since 4.1.0
  • ✅ Realm interface uses noun-style accessors (classLoader(), moduleLayer(), lookup())
  • ✅ PluginManagerException has @Serial serialVersionUID and both constructors
  • ✅ findPlugin() is called outside the try block — IllegalArgumentException propagates unchecked as documented
  • ✅ Realm.lookup() Javadoc accurately describes TCCL-switching behavior (no longer overstates visibility isolation)
  • ✅ DefaultRealm.RealmLookup.scoped() restores TCCL in finally — correct

The null ClassRealm fallback chain in DefaultPlugin (TCCL → class-level classloader) is a safe defensive measure for the case where realm setup hasn't completed.

LGTM.

This review was generated by an AI agent, Hermès on behalf of @gnodet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

v4 SPI: Scoped service discovery from plugin classloaders

3 participants