Conversation
Three major Java IDEs (IntelliJ IDEA, Eclipse m2e, Apache NetBeans) all override the internal PluginDependenciesResolver component for the same reason: they need their workspace reader to not participate in plugin resolution. Plugin realms are cached by DefaultPluginRealmCache (@singleton) and cannot be purged within a session, so resolving plugins from the IDE workspace causes stale classloaders when workspace sources change. This commit introduces a proper SPI in maven-api-spi: org.apache.maven.api.spi.WorkspaceReader The interface uses Maven 4 API types exclusively (no maven-resolver-api dependency required), with three methods: - findArtifact(Artifact): Optional<Path> - findVersions(Artifact): List<String> - isApplicableForPluginResolution(): boolean (default true) IDE integrators can implement this SPI and return false from isApplicableForPluginResolution() to opt their workspace reader out of plugin resolution, without touching any internal Maven component. Implementation: - SpiWorkspaceReaderAdapter bridges SPI implementations into the resolver workspace reader chain (added in DefaultMaven) - DefaultPluginDependenciesResolver filters out non-applicable readers when building plugin sessions - IT mng-8766 verifies that SPI readers with isApplicableForPluginResolution returning false are not called during plugin resolution
gnodet-bot
left a comment
There was a problem hiding this comment.
Clean SPI design that solves a real pain point for all three major IDE integrators. The adapter pattern is sound, the filtering logic is correct, and the IT covers the critical negative case.
One observation on the IT — see inline.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| // The SPI workspace reader should be called for artifact resolution in the main session | ||
| // (e.g., during project dependency resolution, model building, etc.) | ||
| List<String> logLines = verifier.loadLogLines(); |
There was a problem hiding this comment.
💡 The comment says the SPI reader "should be called for artifact resolution" but there's no assertTrue verifying it was called for regular dependency resolution (e.g. findArtifact for junit:junit). The test only asserts the negative case (not called for plugin resolution).
Adding a positive assertion would make the test truly verify both directions of the contract and catch regressions where SPI readers are silently dropped from the main chain entirely.
| // The SPI workspace reader should be called for artifact resolution in the main session | |
| // (e.g., during project dependency resolution, model building, etc.) | |
| List<String> logLines = verifier.loadLogLines(); | |
| // The SPI workspace reader should be called for artifact resolution in the main session | |
| // (e.g., during project dependency resolution, model building, etc.) | |
| List<String> logLines = verifier.loadLogLines(); | |
| boolean hasFindArtifactCalls = | |
| logLines.stream().anyMatch(line -> line.contains("[SPI-WR] findArtifact(")); | |
| assertTrue( | |
| hasFindArtifactCalls, | |
| "SPI workspace reader should be consulted during regular artifact resolution"); |
There was a problem hiding this comment.
Fixed in d1d0447. Added the positive assertion for regular artifact resolution. Also fixed the root cause of the CI failure: SPI workspace readers were injected via constructor (javax.inject), but extensions register their beans in the Maven 4 DI system. Switched to lookup.lookupList() which finds them through the Sisu-DI bridge.
|
I'm not sure this really solve the issue or is wanted. Instead one more want to be able to purge the cache (so no disabling is actually needed) isn't it? |
| default boolean isApplicableForPluginResolution() { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
I would say such a method should be much more generic, e.g. to cover other cases of isApplicableFor(...) so maybe it can get an enum to tell if its a dependency, an extension, a plugin, ... or whatever are the cases. Also it seems odd to have this default implemented and saing IDE should return false. this sounds really odd from an OO/SPI point of view at least.
There was a problem hiding this comment.
The isApplicableForPluginResolution() method was removed entirely in 44e96719ff based on your feedback. The current SPI has no such method — the WorkspaceReader interface now only exposes findArtifact() and findVersions(). The design discussion is captured in the PR thread comment above.
SPI WorkspaceReader implementations loaded from core extensions are registered in the Maven 4 DI system (via @org.apache.maven.api.di.Named). Constructor injection via javax.inject cannot see these beans because they live in a different DI world. Switch to lookup.lookupList() which goes through PlexusContainer; the SisuDiBridgeModule bridges Maven 4 DI beans back into Guice/Sisu, making them visible to Plexus lookups. Also add positive assertion in the IT to verify the SPI workspace reader is actually consulted during regular artifact resolution.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after d1d0447c7a. All prior findings addressed:
- Positive assertion added — the IT now verifies
[SPI-WR] findArtifact(appears in logs during regular resolution, confirming the SPI reader participates in the main session. - Discovery fix — switched from constructor-injected
List<WorkspaceReader>tolookup.lookupList(), which correctly picks up beans registered by core extensions (loaded after container bootstrap). This was the root cause of the CI failure. - Test updated —
DefaultMavenSessionScopeTestupdated to match the new constructor signature.
The adapter (SpiWorkspaceReaderAdapter) correctly bridges resolver ↔ API types. LightweightApiArtifact.key() matches the default Artifact.key() contract. The filtering in DefaultPluginDependenciesResolver creates a new session/chain without mutating the original — thread-safe.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
…Cache.invalidate(Artifact) Instead of filtering workspace readers out of plugin resolution, expose a proper invalidate(Artifact) SPI on PluginRealmCache so IDEs can purge stale plugin realms when a workspace artifact is rebuilt. This approach is more accurate: IDEs trust their own build, so if a plugin has been rebuilt in the workspace, it should be used. The invalidate() method lets the IDE evict the cached realm on demand, rather than blanket-excluding workspace readers from plugin resolution. Changes: - PluginRealmCache: add default invalidate(org.apache.maven.api.Artifact) - DefaultPluginRealmCache: implement invalidate() by evicting matching entries (matched on groupId:artifactId:version) and disposing their ClassRealms - WorkspaceReader SPI: remove isApplicableForPluginResolution() - SpiWorkspaceReaderAdapter: remove isApplicableForPluginResolution() - DefaultPluginDependenciesResolver: remove filterWorkspaceReadersForPluginResolution() - IT: simplify test to verify SPI discovery and artifact resolution consultation
|
Thanks @laeubi, you're right on both counts. The approach has been reworked in 44e96719ff:
The reasoning: IDEs trust their own build — if they've rebuilt a plugin from the workspace, they want it used. The right API is cache invalidation on demand, not blanket exclusion from resolution. As for the generics of |
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after 44e96719ff. The rework based on @laeubi's feedback is a significant improvement — PluginRealmCache.invalidate(Artifact) is the right abstraction (cache invalidation on demand vs. blanket exclusion from resolution).
The SPI interface, adapter, and DI discovery are solid. Two cosmetic issues from the rework — stale <description> elements that still reference the old approach.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| <packaging>jar</packaging> | ||
|
|
||
| <name>Maven Integration Test :: spi-workspace-reader</name> | ||
| <description>SPI WorkspaceReader extension that opts out of plugin resolution</description> |
There was a problem hiding this comment.
💡 Stale description from the previous approach — the extension no longer "opts out of plugin resolution." The SPI now provides workspace artifact resolution, and cache invalidation is handled separately via PluginRealmCache.invalidate().
| <description>SPI WorkspaceReader extension that opts out of plugin resolution</description> | |
| <description>SPI WorkspaceReader extension for IDE workspace artifact resolution</description> |
| <packaging>jar</packaging> | ||
|
|
||
| <name>Maven Integration Test :: mng-8766</name> | ||
| <description>Verify that SPI WorkspaceReader is used for dependency resolution but not for plugin resolution.</description> |
There was a problem hiding this comment.
💡 Same stale description — the IT no longer verifies plugin resolution exclusion.
| <description>Verify that SPI WorkspaceReader is used for dependency resolution but not for plugin resolution.</description> | |
| <description>Verify that SPI WorkspaceReader is discovered and consulted for artifact resolution.</description> |
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after 7b734aae0a. Both stale <description> elements fixed exactly as suggested — extension pom now says "IDE workspace artifact resolution" and project pom says "discovered and consulted for artifact resolution."
All prior findings addressed. SPI design, adapter, cache invalidation, and IT remain solid from the previous review.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after 8850b59ed8. Good fix — moving SPI reader discovery from doExecute() to setupWorkspaceReader() is correct: core extensions aren't in the container until after buildGraph(), so the earlier lookup.lookupList() call always returned empty. Using getProjectScopedExtensionComponents() is consistent with the legacy WorkspaceReader discovery on the line above.
One stale comment from the previous approach — see inline.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| // n+1) SPI workspace readers (org.apache.maven.api.spi) — discovered here, after buildGraph() | ||
| // has loaded core extensions into the container so they are visible via lookupList. |
There was a problem hiding this comment.
💡 Stale comment — this code no longer uses lookupList() directly; it uses getProjectScopedExtensionComponents() which scans project realms.
| // n+1) SPI workspace readers (org.apache.maven.api.spi) — discovered here, after buildGraph() | |
| // has loaded core extensions into the container so they are visible via lookupList. | |
| // n+1) SPI workspace readers (org.apache.maven.api.spi) — discovered here, after buildGraph() | |
| // has loaded core extensions into the container so they are visible via getProjectScopedExtensionComponents. |
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after 438208ff5e (spotless formatting).
The formatting fix is fine, but the stale comment from the previous review is still not addressed — see inline.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| // n+1) SPI workspace readers (org.apache.maven.api.spi) — discovered here, after buildGraph() | ||
| // has loaded core extensions into the container so they are visible via lookupList. |
There was a problem hiding this comment.
💡 Not addressed from previous review — this comment still says "visible via lookupList" but the code uses getProjectScopedExtensionComponents(). Raised in the previous review at 8850b59ed8.
| // n+1) SPI workspace readers (org.apache.maven.api.spi) — discovered here, after buildGraph() | |
| // has loaded core extensions into the container so they are visible via lookupList. | |
| // n+1) SPI workspace readers (org.apache.maven.api.spi) — discovered here, after buildGraph() | |
| // has loaded core extensions into the container so they are visible via getProjectScopedExtensionComponents. |
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after 8bbf426e4c. Stale comment fixed — now correctly references getProjectScopedExtensionComponents instead of lookupList.
All prior findings from previous reviews are addressed. SPI design, adapter, cache invalidation, and IT remain solid.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after 3f224de701 — spotless formatting only (wrapped long comment line in DefaultMaven). No logic change. All prior findings remain addressed.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
3f224de to
b36eba6
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after b36eba696c. The switch from getProjectScopedExtensionComponents() to the maven-di Injector via SpiWorkspaceReadersHolder is the correct fix — SPI components annotated with @org.apache.maven.api.di.Named live in the maven-di layer, not Plexus/SISU, so Plexus lookups always returned empty. The new @Singleton holder receives all named WorkspaceReader bindings via Map<String, WorkspaceReader> injection and is instantiated lazily after buildGraph(), when extension bindings are visible. The META-INF/maven/org.apache.maven.api.di.Inject registration is correct.
No new issues. All prior findings remain addressed.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
The previous approach (getProjectScopedExtensionComponents) failed because SPI components annotated with @org.apache.maven.api.di.Named live in the maven-di layer, not in Plexus/SISU. Plexus's container.lookupList() always returns empty for these types. The fix introduces SpiWorkspaceReadersHolder, a maven-di @nAmed @singleton bridged to Guice/SISU via SisuDiBridgeModule.BridgeInjectorImpl. Being a maven-di component (not @javax.inject.Named), it is not discovered by SISU independently, avoiding container-init ordering issues. The holder receives all named WorkspaceReader SPI bindings via @nullable Map<String, WorkspaceReader> injection — @nullable so that Maven works normally when no extension provides a WorkspaceReader (in which case the injected map is null and the list is empty). The holder is looked up via Lookup.lookup(SpiWorkspaceReadersHolder.class) in setupWorkspaceReader(), which executes after buildGraph() has loaded core extensions and registered their DI bindings.
b36eba6 to
23fb055
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after 23fb055bbc. Two clean fixes:
DefaultMaven— lookup simplified fromlookup.lookup(Injector.class).getInstance(SpiWorkspaceReadersHolder.class)tolookup.lookup(SpiWorkspaceReadersHolder.class). Correct: the holder is@Named @Singletonunder maven-di, bridged to Sisu viaSisuDiBridgeModule, so a plain Plexuslookup()finds it. The comment now accurately describes this mechanism.SpiWorkspaceReadersHolder—@Nullableadded on theMap<String, WorkspaceReader>parameter with a null-safe initialization guard. Correct fix for the zero-readers case: maven-di injectsnullfor an unsatisfiedMap<K,V>binding rather than throwing.
All prior findings remain addressed. No new issues introduced.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after 23fb055bbc. The SpiWorkspaceReadersHolder approach is cleaner than the direct Injector access from b36eba696c — using Lookup.lookup() keeps the code consistent with how other components are looked up in DefaultMaven, and the META-INF/maven/org.apache.maven.api.di.Inject registration is the right registration mechanism.
Two issues found:
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| @Override | ||
| public int compareTo(Version o) { | ||
| return version.compareTo(o.toString()); |
There was a problem hiding this comment.
StringVersion.compareTo() uses String.compareTo() — lexicographic ordering. This breaks for multi-digit version components: "10".compareTo("9") returns a negative value ('1' < '9'), so version 10 would sort before 9.
The contract of org.apache.maven.api.Version requires semantic ordering. Any SPI implementation that sorts or compares versions (e.g. to pick the highest available workspace version) will silently get wrong results.
Since this is a lightweight wrapper whose toString() is used for equality and lookup, and Maven has a proper VersionParser available via the session, the safest fix is to delegate compareTo to a real parser — or at minimum document the limitation explicitly in the Javadoc so implementors know not to rely on it.
| @Override | |
| public int compareTo(Version o) { | |
| return version.compareTo(o.toString()); | |
| private record StringVersion(String version) implements Version { | |
| @Override | |
| public int compareTo(Version o) { | |
| // NOTE: lexicographic comparison only — does not honour semantic version ordering. | |
| // This wrapper is intended for artifact coordinate lookup only (via toString()); | |
| // do not use compareTo() for version range evaluation or sorting. | |
| return version.compareTo(o.toString()); | |
| } |
There was a problem hiding this comment.
Fixed in 4a192fe. Added Javadoc to StringVersion explicitly documenting that compareTo() uses lexicographic string ordering and is not correct for semantic version comparison — directing callers to use VersionParser when ordering matters. Since this wrapper is solely used to pass artifact coordinates to SPI workspace readers (not for version sorting), using a full parser here would add unnecessary session coupling; the documentation makes the limitation explicit for any future reader implementations that might call getVersion().compareTo().
| return readers; | ||
| } |
There was a problem hiding this comment.
💡 getReaders() returns the internal ArrayList directly. While SpiWorkspaceReadersHolder is a @Singleton and the list is never modified after construction, exposing a mutable List through a public method is a correctness risk — any caller can call .add() / .remove() / .clear() and silently corrupt the shared state for subsequent builds in the same session.
Return an unmodifiable view:
| return readers; | |
| } | |
| public List<WorkspaceReader> getReaders() { | |
| return Collections.unmodifiableList(readers); |
There was a problem hiding this comment.
Fixed in 4a192fe. getReaders() now returns Collections.unmodifiableList(readers).
…nmodifiable list from getReaders()
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review after 4a192fef92. Both findings from the previous review are addressed:
getReaders()mutable list —Collections.unmodifiableList(readers)is now returned. Correct; the@Nullablepath that initialisesreaderstoCollections.emptyList()already returns an unmodifiable list, so the double-wrap is harmless.StringVersion.compareTo()Javadoc — The new class-level Javadoc is accurate and complete: it calls out the lexicographic limitation, states that the wrapper is used solely for coordinate lookup, and directs implementors toVersionParserwhen ordering matters.
All prior findings from previous reviews remain addressed. The SPI interface, adapter, holder, DI discovery path, and IT are solid.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
Problem
Three major Java IDEs independently override the same internal Maven component —
PluginDependenciesResolver— for identical reasons:Maven40PluginDependenciesResolver@Priority(10)EclipsePluginDependenciesResolverDefaultPluginDependenciesResolverNbPluginDependenciesResolverDefaultPluginDependenciesResolverThe root cause: m2e's source still carries the original comment from 2009:
DefaultPluginRealmCacheis@Singleton. Plugin realms cannot be purged within a session. If the IDE workspace reader resolves a plugin, the cached realm becomes stale when workspace sources change. The only available workaround is to disable the IDE workspace reader during plugin resolution — which requires overriding an internal component.This was raised in the Maven dev list: [DISCUSS] No supported extension point for plugin/extension resolution (in IDEs)?
Solution
Introduce a proper SPI in
maven-api-spi:maven-resolver-apidependency requiredisApplicableForPluginResolution()lets IDE integrators opt their reader out of plugin resolution without touching any internal componentSpiWorkspaceReaderAdapterbridges SPI implementations into the resolver workspace reader chainDefaultPluginDependenciesResolverfilters out non-applicable readers when building plugin sessionsMigration for IDE integrators
Instead of extending
DefaultPluginDependenciesResolver:Changes
maven-api-spi: newWorkspaceReaderSPI interfacemaven-core:SpiWorkspaceReaderAdapterbridges SPI → resolver;DefaultMaveninjects SPI readers;DefaultPluginDependenciesResolverfilters them per plugin sessionmng-8766: verifies SPI reader withisApplicableForPluginResolution()=falseis not called during plugin resolution