Internalize plexus-classworlds and add JPMS module support (fixes #11028)#11029
Internalize plexus-classworlds and add JPMS module support (fixes #11028)#11029gnodet wants to merge 14 commits into
Conversation
b4c6955 to
fe2ac45
Compare
|
Should we maybe at the same time archive/EOL plexus-classworlds? |
|
I'm not sure if we need copy it to Maven core ... Especially that old java package and old licenses header are preserved |
This is currently a fork, and I removed the old layer already.
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 |
…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.
fe2ac45 to
c5ab33f
Compare
- 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>
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>
- 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>
There was a problem hiding this comment.
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-classworldswith internalmaven-classworlds+ newmaven-api-classworldsAPI and update imports/usages across core/cli/compat. - Update Maven distribution + launcher scripts (
mvn,mvn.cmd,m2.conf, assembly component) to load JLine fromlib/modulesand 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> |
| * @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" ) |
| <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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 theirplugin.xml. This is a hard contract — if the module graph can't be resolved (split packages, missingmodule-info.class, etc.), the build fails withModuleLayerCreationException. No fallback to classpath loading. -
Flat layer hierarchy: All plugin layers are siblings under the same parent layer (the runtime layer from
ClassWorldif available, or the boot layer). No nesting. -
DI instead of ServiceLoader: Rather than
ServiceLoader.load(layer, MyService.class), we use Maven'smaven-diInjector.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
ModuleLayerandControllerare stored on theClassRealm, not onClassWorld— 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
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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-accessexports package from the module that we are loading to other modules, why not using the standardmodule-infomechanism? - If
META-INF/maven/module-accessexports 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.
There was a problem hiding this comment.
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 addexportsto someone else's module descriptor qualified exportsinmodule-info.javarequire 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 aController— 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")); | ||
|
|
There was a problem hiding this comment.
It is not very important, but all those blank lines consume a large number of space for no apparent reason.
There was a problem hiding this comment.
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>
Summary
Replaces the external
plexus-classworldsdependency with an internalizedmaven-classworldsmodule and adds JPMS module support to enable targeted--enable-native-accessfor JLine, replacing the broadALL-UNNAMEDflag.Internalize plexus-classworlds
impl/maven-classworlds, maintaining original package structureorg.apache.maven.api.classworldsinto a separate API module (api/maven-api-classworlds)plexus-classworldsdependencies across the build withmaven-classworldsTargeted
--enable-native-accessfor JLine (fixes #11028)lib/modules/and load them on the boot--module-pathvia a newmoduledirective inm2.conf--add-modules ALL-MODULE-PATHto resolve modules on the module-path--enable-native-access=ALL-UNNAMEDwith targeted--enable-native-access=org.jline.terminal.ffm,org.jline.nativRuntime ModuleLayer with module access control
add-exports,add-opens,add-readsdirectives tom2.confconfiguration parserModuleLayerinConfiguratorfor modules not already in the boot layer, withController-based access controlModuleLayerandControlleronClassWorldfor realm-level accessClassRealm.addExports()andClassRealm.addOpens()methods for plugins/extensions to control module access at runtimeMETA-INF/maven/module-accessdescriptor files inDefaultClassRealmManager.createRealm()— applies to all realm types (plugin, extension, project)Descriptor format:
META-INF/maven/module-accessPlugins and extensions can declare module access requirements:
Files changed (key files)
apache-maven/src/assembly/maven/bin/mvnapache-maven/src/assembly/maven/bin/mvn.cmdapache-maven/src/assembly/maven/bin/m2.confmoduledirective for JLine jarsimpl/maven-classworlds/api/maven-api-classworlds/ConfigurationHandler.javaaddExports,addOpens,addReadscallbacksConfigurationParser.javaadd-exports,add-opens,add-readsdirectivesConfigurator.javaClassWorld.javaClassRealm.javaaddExports(),addOpens()runtime API for pluginsDefaultClassRealmManager.javaMETA-INF/maven/module-accessdescriptor processingTest plan
mvn verify -pl impl/maven-classworlds— 93 tests passmvn install -DskipTests— full build succeedsmvn --version— clean output, no warningsmvn validate— JLine terminal works correctlyCloses #11029
Fixes #11028
Claude Code on behalf of Guillaume Nodet