From d6f91a672b207acb55e060ddb98e3d4a8a19a43f Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Thu, 4 Jun 2026 17:55:07 +0200 Subject: [PATCH] Fix @PreDestroy ClassNotFoundException from premature ClassRealm disposal (#11825) * [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 * 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 --------- Co-authored-by: Claude Opus 4.6 --- .../plugin/DefaultExtensionRealmCache.java | 2 +- .../maven/plugin/DefaultPluginRealmCache.java | 2 +- .../project/DefaultProjectRealmCache.java | 2 +- .../plugin/DefaultRealmCacheDisposeTest.java | 85 +++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 impl/maven-core/src/test/java/org/apache/maven/plugin/DefaultRealmCacheDisposeTest.java 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(); } } 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; + } + } +}