From 295e9f823340f271eb352c7cb3509f8e149e7036 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 23 Mar 2026 13:07:12 +0100 Subject: [PATCH 1/2] [MNG-8572] Fix @PreDestroy ClassNotFoundException caused by premature ClassRealm disposal The Plexus Disposable.dispose() lifecycle runs before Sisu's @PreDestroy callbacks. When dispose() called flush(), it disposed ClassRealms before @PreDestroy methods on beans loaded from those realms could execute, causing ClassNotFoundException. Change dispose() to only clear the cache map without disposing realms. The flush() method (used for explicit cache clearing between builds) remains unchanged. ClassRealms are disposed when the PlexusContainer shuts down after all lifecycle callbacks complete. Co-Authored-By: Claude Opus 4.6 --- .../org/apache/maven/plugin/DefaultExtensionRealmCache.java | 2 +- .../java/org/apache/maven/plugin/DefaultPluginRealmCache.java | 2 +- .../java/org/apache/maven/project/DefaultProjectRealmCache.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultExtensionRealmCache.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultExtensionRealmCache.java index b20211525854..ce3cb5135f3e 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultExtensionRealmCache.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultExtensionRealmCache.java @@ -149,6 +149,6 @@ public void register(MavenProject project, Key key, CacheRecord record) { @Override public void dispose() { - flush(); + cache.clear(); } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultPluginRealmCache.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultPluginRealmCache.java index 681955f21db3..1e822a2ccb0c 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultPluginRealmCache.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/DefaultPluginRealmCache.java @@ -215,6 +215,6 @@ public void register(MavenProject project, Key key, CacheRecord record) { @Override public void dispose() { - flush(); + cache.clear(); } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectRealmCache.java b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectRealmCache.java index 82a1a814c1b0..9111177c3489 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectRealmCache.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectRealmCache.java @@ -125,6 +125,6 @@ public void register(MavenProject project, Key key, CacheRecord record) { @Override public void dispose() { - flush(); + cache.clear(); } } From f4984cbfffb96bf52029d4a2b53b143e382eeb7f Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 1 Jun 2026 16:24:28 +0200 Subject: [PATCH 2/2] Add test for dispose() vs flush() ClassRealm behavior Verifies that dispose() clears the cache without disposing ClassRealms (so @PreDestroy callbacks can still execute), while flush() disposes both the cache and the realms. Co-Authored-By: Claude Opus 4.6 --- .../plugin/DefaultRealmCacheDisposeTest.java | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 impl/maven-core/src/test/java/org/apache/maven/plugin/DefaultRealmCacheDisposeTest.java diff --git a/impl/maven-core/src/test/java/org/apache/maven/plugin/DefaultRealmCacheDisposeTest.java b/impl/maven-core/src/test/java/org/apache/maven/plugin/DefaultRealmCacheDisposeTest.java new file mode 100644 index 000000000000..4edf41a61a89 --- /dev/null +++ b/impl/maven-core/src/test/java/org/apache/maven/plugin/DefaultRealmCacheDisposeTest.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.maven.plugin; + +import java.util.List; + +import org.apache.maven.artifact.Artifact; +import org.codehaus.plexus.classworlds.ClassWorld; +import org.codehaus.plexus.classworlds.realm.ClassRealm; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that dispose() does not dispose ClassRealms prematurely. + *

+ * Plexus Disposable.dispose() runs before Sisu's @PreDestroy callbacks. + * If dispose() disposes ClassRealms, beans loaded from those realms will + * get ClassNotFoundException when their @PreDestroy methods execute. + * dispose() should only clear the cache map; flush() should dispose realms. + * + * @see MNG-8572 + */ +class DefaultRealmCacheDisposeTest { + + @Test + void disposeDoesNotDisposeClassRealms() throws Exception { + ClassWorld world = new ClassWorld(); + ClassRealm realm = world.newRealm("test-plugin-realm"); + + DefaultPluginRealmCache cache = new DefaultPluginRealmCache(); + PluginRealmCache.CacheRecord record = new PluginRealmCache.CacheRecord(realm, List.of()); + cache.cache.put(new TestKey(), record); + + cache.dispose(); + + assertTrue(cache.cache.isEmpty(), "dispose() should clear the cache"); + assertNotNull(world.getClassRealm("test-plugin-realm"), "dispose() should NOT dispose the ClassRealm"); + } + + @Test + void flushDisposesClassRealms() throws Exception { + ClassWorld world = new ClassWorld(); + ClassRealm realm = world.newRealm("test-plugin-realm-flush"); + + DefaultPluginRealmCache cache = new DefaultPluginRealmCache(); + PluginRealmCache.CacheRecord record = new PluginRealmCache.CacheRecord(realm, List.of()); + cache.cache.put(new TestKey(), record); + + cache.flush(); + + assertTrue(cache.cache.isEmpty(), "flush() should clear the cache"); + assertNull(world.getClassRealm("test-plugin-realm-flush"), "flush() SHOULD dispose the ClassRealm"); + } + + private static class TestKey implements PluginRealmCache.Key { + @Override + public int hashCode() { + return 1; + } + + @Override + public boolean equals(Object obj) { + return obj instanceof TestKey; + } + } +}