Skip to content

Internalize plexus-classworlds and add JPMS module support (fixes #11028)#11029

Draft
gnodet wants to merge 14 commits into
apache:masterfrom
gnodet:feature/maven-classworlds-11028
Draft

Internalize plexus-classworlds and add JPMS module support (fixes #11028)#11029
gnodet wants to merge 14 commits into
apache:masterfrom
gnodet:feature/maven-classworlds-11028

Conversation

@gnodet

@gnodet gnodet commented Aug 7, 2025

Copy link
Copy Markdown
Contributor

Summary

Replaces the external plexus-classworlds dependency with an internalized maven-classworlds module and adds JPMS module support to enable targeted --enable-native-access for JLine, replacing the broad ALL-UNNAMED flag.

Internalize plexus-classworlds

  • Copy plexus-classworlds source into impl/maven-classworlds, maintaining original package structure
  • Extract org.apache.maven.api.classworlds into a separate API module (api/maven-api-classworlds)
  • Replace all plexus-classworlds dependencies across the build with maven-classworlds
  • Fully eliminate plexus-classworlds from the dependency tree

Targeted --enable-native-access for JLine (fixes #11028)

  • Move JLine jars to lib/modules/ and load them on the boot --module-path via a new module directive in m2.conf
  • Add --add-modules ALL-MODULE-PATH to resolve modules on the module-path
  • Replace --enable-native-access=ALL-UNNAMED with targeted --enable-native-access=org.jline.terminal.ffm,org.jline.nativ
  • Add Java version detection in launcher scripts — skip module flags on JDKs that don't support them

Runtime ModuleLayer with module access control

  • Add add-exports, add-opens, add-reads directives to m2.conf configuration parser
  • Create a runtime ModuleLayer in Configurator for modules not already in the boot layer, with Controller-based access control
  • Store ModuleLayer and Controller on ClassWorld for realm-level access
  • Add ClassRealm.addExports() and ClassRealm.addOpens() methods for plugins/extensions to control module access at runtime
  • Process META-INF/maven/module-access descriptor files in DefaultClassRealmManager.createRealm() — applies to all realm types (plugin, extension, project)

Descriptor format: META-INF/maven/module-access

Plugins and extensions can declare module access requirements:

# Export a package from a named module to the plugin's classloader
add-exports org.jline.terminal/org.jline.terminal.spi
# Open a package for deep reflection
add-opens org.jline.reader/org.jline.reader.impl

Files changed (key files)

File Change
apache-maven/src/assembly/maven/bin/mvn Module-path, add-modules, targeted enable-native-access
apache-maven/src/assembly/maven/bin/mvn.cmd Same for Windows
apache-maven/src/assembly/maven/bin/m2.conf module directive for JLine jars
impl/maven-classworlds/ Full internalized classworlds implementation
api/maven-api-classworlds/ New classworlds API module
ConfigurationHandler.java addExports, addOpens, addReads callbacks
ConfigurationParser.java Parse add-exports, add-opens, add-reads directives
Configurator.java Runtime ModuleLayer creation and directive application
ClassWorld.java ModuleLayer + Controller storage
ClassRealm.java addExports(), addOpens() runtime API for plugins
DefaultClassRealmManager.java META-INF/maven/module-access descriptor processing

Test plan

  • mvn verify -pl impl/maven-classworlds — 93 tests pass
  • mvn install -DskipTests — full build succeeds
  • mvn --version — clean output, no warnings
  • mvn validate — JLine terminal works correctly

Closes #11029
Fixes #11028

Claude Code on behalf of Guillaume Nodet

@gnodet gnodet force-pushed the feature/maven-classworlds-11028 branch 3 times, most recently from b4c6955 to fe2ac45 Compare August 9, 2025 16:07
@cstamas

cstamas commented Aug 11, 2025

Copy link
Copy Markdown
Member

Should we maybe at the same time archive/EOL plexus-classworlds?
Also, is this a fork of it? Or rewrite? As if some third-party may use plexus-classworlds, is this new module drop in replacement?

@slawekjaranowski

Copy link
Copy Markdown
Member

I'm not sure if we need copy it to Maven core ...
Why not simply update plexus-classworlds?

Especially that old java package and old licenses header are preserved

@gnodet

gnodet commented Aug 12, 2025

Copy link
Copy Markdown
Contributor Author

Should we maybe at the same time archive/EOL plexus-classworlds?
Also, is this a fork of it? Or rewrite? As if some third-party may use plexus-classworlds, is this new module drop in replacement?

This is currently a fork, and I removed the old layer already.

I'm not sure if we need copy it to Maven core ... Why not simply update plexus-classworlds?

Especially that old java package and old licenses header are preserved

This was really a first step. I was toying a bit to solve a few limitations:

One important point is that extensions should be able to provide controlled access to some API, which could be consumed by plugins. Plugins themselves could export some packages, I think we had some use cases for that (we currently need an extension to do that).

So not sure yet, this is just experiments... but ideas welcomed. Overall, I think plexus-classworlds won't be able to solve the above problems, and given Maven is really the only user afaik, I'm tempted to merge ...

gnodet added 2 commits June 24, 2026 09:30
…assworld to separate API module

- Remove jline-native references from Maven launcher scripts (mvn and mvn.cmd)
- Remove jline-native dependencies and configurations from POMs
- Remove module-info.java file from maven-classworlds
- Remove jline-native assembly configurations and README
- Create new maven-api-classworlds module in api/ directory
- Move org.apache.maven.api.classworlds package to new API module
- Update dependency management and module references
- Add maven-api-classworlds dependency to modules that use the API

This change separates the public API from the implementation, following Maven 4
architecture patterns, and removes the JPMS and jline-native dependencies that
were causing complexity in the build and runtime environment.
@gnodet gnodet force-pushed the feature/maven-classworlds-11028 branch from fe2ac45 to c5ab33f Compare June 24, 2026 07:32
gnodet and others added 3 commits June 24, 2026 11:00
- Exclude plexus-classworlds transitive dependency from Sisu and
  plexus-testing in root pom dependencyManagement
- Add maven-classworlds test dependency to maven-resolver-provider
  (needed by plexus-testing at runtime)
- Copy legacy org.codehaus.classworlds adapter classes into
  maven-classworlds (required by Sisu's ClassRealmConverter and
  AbstractComponentConfigurator)
- Add checkstyle exclusion for legacy package and RAT exclusion
  for test data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move JLine jars from lib/ to lib/modules/ and load them via
--module-path + --add-modules ALL-MODULE-PATH so that
--enable-native-access can target org.jline.terminal.ffm and
org.jline.nativ by name instead of using the broad ALL-UNNAMED.

Changes:
- Add `module` directive to classworlds ConfigurationParser with
  glob support, mirroring the existing `load` directive
- Separate JLine jars into lib/modules/ in the assembly descriptor
- Update m2.conf with `module ${maven.home}/lib/modules/*.jar`
- Update mvn/mvn.cmd launcher scripts to use --module-path,
  --add-modules ALL-MODULE-PATH, and targeted --enable-native-access
- Update LICENSE.vm to classify JLine under lib/modules/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add JPMS ModuleLayer creation in classworlds for runtime module loading
with add-exports/add-opens/add-reads directives in m2.conf. Plugins and
extensions can declare module access requirements via
META-INF/maven/module-access descriptors, processed for all realm types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnodet gnodet changed the title Add maven-classworlds module to replace plexus-classworlds dependency [ISSUE-11028] Internalize plexus-classworlds and add JPMS module support Jun 24, 2026
gnodet and others added 2 commits June 24, 2026 15:56
Move module access logic from ClassRealm into ClassWorld to encapsulate
the Controller. Add addExports/addOpens/addReads to the API interface as
default methods returning boolean for success feedback. Synchronize
setModuleLayer. Support add-reads in module-access descriptors and log
on failure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use void return type for addExports/addOpens/addReads to preserve
  binary compatibility on the ClassRealm class
- Remove redundant @SInCE tags (interface already has @SInCE 4.1.0)
- Conditionally enable --module-path and --enable-native-access only
  on Java 21+ (FFM API not available on Java 17)
- Add classpath fallback in Configurator when ModuleLayer creation
  fails (e.g. module jars reference unavailable APIs)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnodet gnodet changed the title [ISSUE-11028] Internalize plexus-classworlds and add JPMS module support Internalize plexus-classworlds and add JPMS module support (fixes #11028) Jun 24, 2026
gnodet and others added 6 commits June 24, 2026 17:17
- Include --module-path in launcher probe so it fails correctly on JDKs
  that cannot read the module descriptors (class version 66.0)
- Revert CustomComponentConfigurator import to impl ClassRealm (needed
  for @OverRide compatibility with AbstractComponentConfigurator)
- Add maven-classworlds to ReactorGraph cluster patterns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…flection compat

Launcher and maven-executor use reflection to find main(String[], ClassWorld)
using the impl ClassWorld class. The API ClassWorld type doesn't match, causing
NoSuchMethodException at runtime. Use FQ org.codehaus.plexus.classworlds.ClassWorld
in all main() method parameters while keeping the API import for ProtoLookup mapping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Build the classpath from individual jar files instead of using a wildcard,
since cygpath --windows with a wildcard in the path can cause issues on
MinGW/Git Bash. Also convert MODULES_DIR to Windows format and construct
MAVEN_MODULE_OPTS after path conversion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ClassRealmManagerDelegate.setupRealm() is an SPI that extensions implement.
Changing its ClassRealm parameter type from impl to API breaks pre-compiled
extensions. Revert the interface to use the impl ClassRealm type and cast
in DefaultClassRealmManager. Also revert IT test sources and remove
temporary CI debug logging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JLine jars were exclusively placed in lib/modules/ for JPMS module
path usage, but the EmbeddedMavenExecutor constructs its classpath
from lib/ only. Include JLine in both locations — the JVM's module
system prefers module path over classpath, so there's no conflict
when both paths are active.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Copilot AI 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.

Pull request overview

This PR internalizes plexus-classworlds into a new maven-classworlds implementation module, introduces a new public API module (maven-api-classworlds), and updates Maven’s distribution/launching to support JPMS modules (notably loading JLine from a module path to enable targeted --enable-native-access).

Changes:

  • Replace external plexus-classworlds with internal maven-classworlds + new maven-api-classworlds API and update imports/usages across core/cli/compat.
  • Update Maven distribution + launcher scripts (mvn, mvn.cmd, m2.conf, assembly component) to load JLine from lib/modules and use targeted native-access flags.
  • Add/port classworlds tests and legacy compatibility shims (org.codehaus.classworlds.*) to preserve ecosystem compatibility (e.g., Sisu).

Reviewed changes

Copilot reviewed 141 out of 161 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/graph/ReactorGraph.java Include maven-classworlds in clustering
pom.xml Swap deps to maven-classworlds and add exclusions
its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java Import reorder/update for classworlds
its/core-it-suite/src/test/resources/mng-5771-core-extensions/repo-src/maven-it-core-extensions-no-descriptor/src/main/java/org/apache/maven/its/core_extensions/TestClassRealmManagerDelegate.java Import reorder/update for classworlds
its/core-it-suite/src/test/resources/mng-5530-mojo-execution-scope/extension/src/main/java/org/apache/maven/its/mng5530/mojoexecutionscope/extension/TestClassRealmManagerDelegate.java Import reorder/update for classworlds
impl/pom.xml Add maven-classworlds module
impl/maven-core/src/test/java/org/apache/maven/plugin/PluginManagerTest.java Switch to Maven ClassRealm API
impl/maven-core/src/test/java/org/apache/maven/lifecycle/internal/stub/BuildPluginManagerStub.java Switch to Maven ClassRealm API
impl/maven-core/src/test/java/org/apache/maven/classrealm/DefaultClassRealmManagerTest.java Update realm construction for new API
impl/maven-core/src/main/resources/META-INF/maven/extension.xml Export maven-classworlds instead of plexus
impl/maven-core/src/main/java/org/apache/maven/project/ProjectRealmCache.java Switch to Maven ClassRealm API
impl/maven-core/src/main/java/org/apache/maven/project/MavenProject.java Switch to Maven ClassRealm API
impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectRealmCache.java Switch to Maven ClassRealm API/exceptions
impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuildingHelper.java Use ClassRealm#getClassLoader + import updates
impl/maven-core/src/main/java/org/apache/maven/plugin/PluginRealmCache.java Switch to Maven ClassRealm API
impl/maven-core/src/main/java/org/apache/maven/plugin/PluginManagerException.java Switch to Maven NoSuchRealmException API
impl/maven-core/src/main/java/org/apache/maven/plugin/PluginContainerException.java Switch to Maven ClassRealm API
impl/maven-core/src/main/java/org/apache/maven/plugin/ExtensionRealmCache.java Switch to Maven ClassRealm API
impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultPluginRealmCache.java Switch to Maven ClassRealm API/exceptions
impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultExtensionRealmCache.java Switch to Maven ClassRealm API/exceptions
impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultBuildPluginManager.java Use ClassRealm#getClassLoader in execution paths
impl/maven-core/src/main/java/org/apache/maven/plugin/BuildPluginManager.java Switch to Maven ClassRealm API
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/LifecycleDependencyResolver.java Use ClassRealm#getClassLoader for TCCL
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java Use ClassRealm#getClassLoader for TCCL
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/builder/BuilderCommon.java Use ClassRealm#getClassLoader for TCCL
impl/maven-core/src/main/java/org/apache/maven/internal/impl/internal/DefaultCoreRealm.java Switch to Maven ClassRealm API
impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultProject.java Use realm.getClassLoader for TCCL
impl/maven-core/src/main/java/org/apache/maven/internal/CoreRealm.java Switch to Maven ClassWorld/ClassRealm API
impl/maven-core/src/main/java/org/apache/maven/extension/internal/CoreExtensionEntry.java Use realm.getClassLoader for resource lookup
impl/maven-core/src/main/java/org/apache/maven/extension/internal/CoreExports.java Store exports as ClassLoader map
impl/maven-core/src/main/java/org/apache/maven/DefaultMaven.java Use realm.getClassLoader for scanning
impl/maven-core/src/main/java/org/apache/maven/configuration/internal/EnhancedComponentConfigurator.java Pass ClassLoader instead of casting realm
impl/maven-core/src/main/java/org/apache/maven/classrealm/ClassRealmManager.java Switch to Maven ClassRealm API
impl/maven-core/pom.xml Add revapi excludes for signature changes
impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/resident/ResidentMavenInvokerTest.java Switch to Maven ClassWorld API
impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTestSupport.java Switch to Maven ClassWorld API + impl wiring
impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/MavenInvokerTest.java Switch to Maven ClassWorld API
impl/maven-cli/src/main/java/org/apache/maven/cling/MavenUpCling.java Update ClassWorld typing in entrypoints
impl/maven-cli/src/main/java/org/apache/maven/cling/MavenShellCling.java Update ClassWorld typing in entrypoints
impl/maven-cli/src/main/java/org/apache/maven/cling/MavenEncCling.java Update ClassWorld typing in entrypoints
impl/maven-cli/src/main/java/org/apache/maven/cling/MavenCling.java Update ClassWorld typing in entrypoints
impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/PlexusContainerCapsuleFactory.java Bridge Maven API realms to Plexus container
impl/maven-cli/src/main/java/org/apache/maven/cling/extensions/BootstrapCoreExtensionManager.java Use ClassLoader-based realm imports/parenting
impl/maven-cli/src/main/java/org/apache/maven/cling/ClingSupport.java Construct impl ClassWorld explicitly
impl/maven-cli/pom.xml Depend on maven-api-classworlds + maven-classworlds
impl/maven-classworlds/src/test/test-data/valid.conf Add parser test data
impl/maven-classworlds/src/test/test-data/valid-launch.conf Add launcher test data
impl/maven-classworlds/src/test/test-data/valid-launch-exitCode.conf Add launcher test data
impl/maven-classworlds/src/test/test-data/valid-from-from-from.conf Add parser test data
impl/maven-classworlds/src/test/test-data/valid-enh-launch.conf Add launcher test data
impl/maven-classworlds/src/test/test-data/valid-enh-launch-exitCode.conf Add launcher test data
impl/maven-classworlds/src/test/test-data/unhandled.conf Add parser test data
impl/maven-classworlds/src/test/test-data/set-using-nonexistent.conf Add parser test data
impl/maven-classworlds/src/test/test-data/set-using-missing.conf Add parser test data
impl/maven-classworlds/src/test/test-data/set-using-existent.properties Add parser test data
impl/maven-classworlds/src/test/test-data/set-using-existent.conf Add parser test data
impl/maven-classworlds/src/test/test-data/resources/classworlds.conf Add resource test data
impl/maven-classworlds/src/test/test-data/realm-syntax.conf Add parser test data
impl/maven-classworlds/src/test/test-data/optionally-nonexistent.conf Add parser test data
impl/maven-classworlds/src/test/test-data/optionally-existent.conf Add parser test data
impl/maven-classworlds/src/test/test-data/nested.properties Add resource test data
impl/maven-classworlds/src/test/test-data/launch-nomethod.conf Add launcher test data
impl/maven-classworlds/src/test/test-data/launch-noclass.conf Add launcher test data
impl/maven-classworlds/src/test/test-data/inheritance.conf Add parser test data
impl/maven-classworlds/src/test/test-data/early-import.conf Add parser test data
impl/maven-classworlds/src/test/test-data/dupe-realm.conf Add parser test data
impl/maven-classworlds/src/test/test-data/dupe-main.conf Add parser test data
impl/maven-classworlds/src/test/test-data/a.properties Add resource test data
impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/UrlUtilsTest.java Add UrlUtils unit test
impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/TestUtil.java Add test utility
impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/strategy/StrategyTest.java Add strategy unit tests
impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/realm/FilteredClassRealmTest.java Add filtered realm tests
impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/LauncherTest.java Add launcher tests
impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/launcher/ConfigurationParserTest.java Add parser tests
impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/ClassWorldTest.java Add ClassWorld tests
impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/ClassView.java Add legacy debug utility
impl/maven-classworlds/src/test/java/org/codehaus/plexus/classworlds/AbstractClassWorldsTestCase.java Add shared test base
impl/maven-classworlds/src/test/java/org/apache/maven/api/classworlds/ApiTest.java Add API usage test
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/UrlUtils.java Add internalized UrlUtils
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/StrategyFactory.java Add internalized strategy factory
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/Strategy.java Bridge impl Strategy to API Strategy
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/SelfFirstStrategy.java Add internalized strategy
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/ParentFirstStrategy.java Add internalized strategy
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/OsgiBundleStrategy.java Add internalized strategy
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/strategy/AbstractStrategy.java Add internalized base strategy
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/NoSuchRealmException.java Bridge impl exception to API exception
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/FilteredClassRealm.java Add filtered realm implementation
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/realm/DuplicateRealmException.java Bridge impl exception to API exception
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationHandler.java Extend config handler for modules/exports/opens/reads
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/launcher/ConfigurationException.java Add internalized config exception
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorldListener.java Bridge impl listener to API listener
impl/maven-classworlds/src/main/java/org/codehaus/plexus/classworlds/ClassWorldException.java Bridge impl exception to API exception
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/package-info.java Document legacy API compatibility constraints
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/NoSuchRealmException.java Legacy compatibility exception
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/Launcher.java Legacy compatibility launcher wrapper
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/DuplicateRealmException.java Legacy compatibility exception
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/DefaultClassRealm.java Legacy compatibility realm wrapper
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ConfiguratorAdapter.java Legacy compatibility adapter
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/Configurator.java Legacy compatibility configurator
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ConfigurationException.java Legacy compatibility exception
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldReverseAdapter.java Legacy reverse adapter
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldException.java Legacy compatibility exception
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorldAdapter.java Legacy adapter
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassWorld.java Legacy compatibility ClassWorld
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealmReverseAdapter.java Legacy reverse adapter
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealmAdapter.java Legacy adapter for Sisu compatibility
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/ClassRealm.java Legacy interface for Sisu compatibility
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/BytesURLStreamHandler.java Legacy byte-array URL handler
impl/maven-classworlds/src/main/java/org/codehaus/classworlds/BytesURLConnection.java Legacy byte-array URL connection
impl/maven-classworlds/pom.xml New module build/test configuration
compat/maven-resolver-provider/pom.xml Add maven-classworlds test dependency
compat/maven-plugin-api/pom.xml Replace plexus-classworlds dependency
compat/maven-embedder/src/main/java/org/apache/maven/cli/MavenCli.java Bridge API ClassWorld to Plexus container
compat/maven-embedder/src/main/java/org/apache/maven/cli/internal/BootstrapCoreExtensionManager.java Use ClassLoader-based realm imports/parenting
compat/maven-embedder/src/main/java/org/apache/maven/cli/CliRequest.java Switch to Maven ClassWorld API
compat/maven-embedder/pom.xml Depend on maven-api-classworlds + maven-classworlds
compat/maven-compat/pom.xml Depend on maven-api-classworlds + maven-classworlds
api/pom.xml Register new maven-api-classworlds module
api/maven-api-classworlds/src/main/java/org/apache/maven/api/classworlds/Strategy.java New API Strategy interface
api/maven-api-classworlds/src/main/java/org/apache/maven/api/classworlds/package-info.java New API package docs
api/maven-api-classworlds/src/main/java/org/apache/maven/api/classworlds/NoSuchRealmException.java New API exception
api/maven-api-classworlds/src/main/java/org/apache/maven/api/classworlds/DuplicateRealmException.java New API exception
api/maven-api-classworlds/src/main/java/org/apache/maven/api/classworlds/ClassWorldListener.java New API listener
api/maven-api-classworlds/src/main/java/org/apache/maven/api/classworlds/ClassWorldException.java New API base exception
api/maven-api-classworlds/src/main/java/org/apache/maven/api/classworlds/ClassWorld.java New API ClassWorld interface
api/maven-api-classworlds/pom.xml New API module POM
apache-maven/src/main/appended-resources/META-INF/LICENSE.vm Adjust license output dirs for boot/modules
apache-maven/src/assembly/maven/bin/mvn.cmd Add module-path + targeted native access
apache-maven/src/assembly/maven/bin/mvn Add module-path + targeted native access
apache-maven/src/assembly/maven/bin/m2.conf Add module directive for lib/modules
apache-maven/src/assembly/component.xml Add boot/module dependency sets

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
<argLine>-ea:org.codehaus.classworlds:org.codehaus.plexus.classworlds</argLine>
Comment on lines +56 to +57
* @param lineNo The number of configuraton line where the problem occured.
* @param line The configuration line where the problem occured.
#* *###
#* *### Classworlds is in boot directory, not in lib
#* *##if ( $project.artifact.artifactId == "plexus-classworlds" )
#* *##if ( $project.artifact.artifactId == "maven-classworlds" )
Comment on lines 39 to 45
<dependencySet>
<useProjectArtifact>false</useProjectArtifact>
<outputDirectory>lib</outputDirectory>
<excludes>
<exclude>org.codehaus.plexus:plexus-classworlds</exclude>
<exclude>org.apache.maven:maven-api-classworlds</exclude>
<exclude>org.apache.maven:maven-classworlds</exclude>
</excludes>
/**
* A class loading realm that provides isolated class loading with controlled imports and exports.
* <p>
* A ClassRealm represents an isolated class loading environment with its own classpath

@desruisseaux desruisseaux Jun 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It seems to duplicate Java module boundaries. Is it a workaround because Maven is not fully modularized yet? (i.e., it still have dependencies on the class-path instead of the module-path).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The ClassRealm concept is not a duplication of JPMS boundaries — the two address different problems and are complementary here.

ClassRealms are Maven's existing dynamic class-loading isolation mechanism (inherited from Plexus classworlds, ~20 years old). They exist because Maven loads plugins and extensions dynamically at runtime, each with its own dependency tree and potentially conflicting transitive dependencies. This is fundamentally a runtime-dynamic problem: we don't know which plugins will be loaded until we read the POM being built. JPMS resolves its module graph statically at startup, so it can't replace this.

The addExports/addOpens/addReads methods on ClassRealm are specifically the bridge between the two worlds: since plugin classes live in realms (classpath-based, unnamed module), these methods use the ModuleLayer.Controller we create internally to grant access from named JPMS modules to the plugin's classloader. For example, when a plugin needs to access JLine's org.jline.terminal.spi package, addExports("org.jline.terminal", "org.jline.terminal.spi") uses the controller to open that access to the realm's unnamed module.

And yes, we do create a ModuleLayer internally (in Configurator.createModuleLayer()) for jars placed on the module-path (like JLine), but it's not exposed in the API. The API only exposes the realm-level addExports/addOpens/addReads as a simpler abstraction for plugins to declare what module access they need — typically via META-INF/maven/module-access descriptors rather than direct API calls.

So to answer directly: it's not a workaround for Maven not being modularized. Even if all Maven jars had module-info.java, we'd still need dynamic class isolation for plugins with conflicting dependencies, and we'd still need a bridge mechanism for plugins to access named modules at runtime.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

JPMS resolves its module graph statically at startup, so it can't replace this.

Java Module can load modules dynamically too, with steps like below:

Path[]        files   = ...;    // Paths to the JAR files of the plugins and dependencies to load.
URL[]         urls    = ...;    // Above paths converted to URLs.
ModuleFinder  modules = ModuleFinder.of(files);
Class<?>      core    = ...;    // Some Maven core class.
ClassLoader   loader  = new URLClassLoader(urls, core.getClassLoader());
ModuleLayer   parent  = core.getModule().getLayer();
Configuration config  = parent.configuration().resolveAndBind(ModuleFinder.of(), modules, getAllModuleNames(modules));
ModuleLayer   layer   = parent.defineModulesWithOneLoader(config, loader);

With the following helper method:

/**
 * Returns the names of all modules that the given finder can see.
 */
private static Set<String> getAllModuleNames(final ModuleFinder modules) {
    final var names = new HashSet<String>();
    for (ModuleReference ref : modules.findAll()) {
        names.add(ref.descriptor().name());
    }
    return names;
}

Then we can discover services (e.g., MOJO implementations) provided by the plugin:

for (MyService service : ServiceLoader.load(layer, MyService.class)) {
    // ...
}

Does something like that has been tried?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes! This is precisely the approach we just implemented for modular plugins in this PR — see `DefaultClassRealmManager.createModularPluginRealm()`. The code is almost identical to your sketch:

ModuleFinder finder = ModuleFinder.of(modulePaths.toArray(new Path[0]));
Set<String> moduleNames = finder.findAll().stream()
        .map(ref -> ref.descriptor().name())
        .collect(Collectors.toSet());

ModuleLayer parentLayer =
        implWorld.getModuleLayer() != null ? implWorld.getModuleLayer() : ModuleLayer.boot();

Configuration cfg = parentLayer.configuration().resolveAndBind(ModuleFinder.of(), finder, moduleNames);

ModuleLayer.Controller controller =
        ModuleLayer.defineModulesWithOneLoader(cfg, List.of(parentLayer), parent);

A few design decisions worth noting:

  • Opt-in via plugin descriptor: Plugin authors declare <modular>true</modular> in their plugin.xml. This is a hard contract — if the module graph can't be resolved (split packages, missing module-info.class, etc.), the build fails with ModuleLayerCreationException. No fallback to classpath loading.

  • Flat layer hierarchy: All plugin layers are siblings under the same parent layer (the runtime layer from ClassWorld if available, or the boot layer). No nesting.

  • DI instead of ServiceLoader: Rather than ServiceLoader.load(layer, MyService.class), we use Maven's maven-di Injector.discover() with the layer's classloader. This integrates modular plugins into Maven's existing injection infrastructure. No Plexus/Sisu for modular plugins.

  • Per-realm storage: The ModuleLayer and Controller are stored on the ClassRealm, not on ClassWorld — each plugin gets its own isolated layer/loader, and the layer's classloader is closed when the realm is disposed.

So yes, dynamic module layers work well for this. Thanks for the suggestion — it validated the direction we were already heading.

@desruisseaux desruisseaux left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My main concern is this META-INF/maven/module-access file parsed by the applyModuleAccessDescriptors(ClassRealm) method in impl/maven-core/src/main/java/org/apache/maven/classrealm/DefaultClassRealmManager.java. I would like to see a documentation about what is the intent, which module is exporting to which module, and why it is not done in module-info.


private static final String MODULE_ACCESS_DESCRIPTOR = "META-INF/maven/module-access";

private void applyModuleAccessDescriptors(ClassRealm classRealm) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have a documentation somewhere of what this code is doing? It seems to open or export packages of a module, but I'm not sure in which direction:

  • If META-INF/maven/module-access exports package from the module that we are loading to other modules, why not using the standard module-info mechanism?
  • If META-INF/maven/module-access exports package from another module to the module that we are loading, it breaks encapsulation principles. Like allowing class A to decide what is private inside class B.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question. The direction is your second option: META-INF/maven/module-access opens/exports packages from a named module in the boot/runtime layer to the plugin's classloader (which lives in the unnamed module).

A concrete example: JLine ships as named modules (org.jline.terminal, org.jline.reader, etc.) in Maven's lib/modules/ directory. A plugin running on the classpath (unnamed module) that needs to interact with JLine's SPI would include:

# META-INF/maven/module-access
add-exports org.jline.terminal/org.jline.terminal.spi=ALL-UNNAMED
add-opens org.jline.terminal/org.jline.terminal.impl=ALL-UNNAMED

This is semantically identical to passing --add-exports org.jline.terminal/org.jline.terminal.spi=ALL-UNNAMED on the JVM command line — it's the standard JPMS mechanism for granting the unnamed module access to packages from named modules. We just read it from a descriptor instead of requiring JVM flags.

As for why the standard module-info mechanism doesn't work here:

  • The plugin doesn't own the module it needs access to (e.g., org.jline.terminal), so it can't add exports to someone else's module descriptor
  • qualified exports in module-info.java require knowing the target module name at compile time, but the plugin is on the classpath (unnamed module, no name to export to)
  • The only standard way to do this at runtime is --add-exports / Module.addExports() via a Controller — which is exactly what the implementation calls under the hood

You're right that this lets class A decide what's visible inside class B's module. That's inherent to the problem: plugins need access to packages that named modules don't export to ALL-UNNAMED by default. The same tension exists with --add-exports on the JVM command line. The module-access descriptor at least makes this explicit and declarative — Maven logs a debug message for each directive applied, so it's auditable.

For modular plugins (the new <modular>true</modular> path), this is less of an issue because the plugin itself is a named module and can use proper requires directives in its own module-info.java. The module-access mechanism is primarily needed for classic (classpath-based) plugins that need to interact with modularized libraries.

mainRealm.importFrom("werkflow", "com.werken.werkflow");

assertSame(werkflowRealm, mainRealm.getImportClassLoader("com.werken.werkflow.WerkflowEngine"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is not very important, but all those blank lines consume a large number of space for no apparent reason.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — these came from the original plexus-classworlds fork. Will clean up.

…gins

Plugins that declare <modular>true</modular> in their descriptor are loaded
in their own ModuleLayer via resolveAndBind, with no fallback to classpath.
Non-modular plugins are unchanged (ClassRealm/URLClassLoader as before).

- Add `modular` boolean field to plugin descriptor model (plugin.mdo)
- Add isModular()/getModuleLayer() to ClassRealm API interface
- Add per-realm ModuleLayer/Controller storage in ClassRealm impl
- Add createModularPluginRealm() to ClassRealmManager/DefaultClassRealmManager
  using ModuleFinder + resolveAndBind + defineModulesWithOneLoader
- Add ModuleLayerCreationException for hard errors (split packages, etc.)
- Update DefaultMavenPluginManager: modular plugins skip Sisu/Plexus
  discovery and use maven-di Injector with the layer's classloader
- Set TCCL to ModuleLayer's loader for modular plugin execution
- Close layer's classloader on realm disposal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request mvn4

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade to JLine 4.x and Replace Broad Native Access with Targeted Module Access

5 participants