From 3c20f3a2c13784e565522127aba1d66600974a83 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Mon, 5 Oct 2026 22:19:21 +0200 Subject: [PATCH] Remove JUL handlers Maven installed when closing the embedded executor Maven 4 puts an SLF4JBridgeHandler on the JVM-global JUL root logger. Closing the executor closes its class realm, so the next JUL record logged in the JVM (JUnit's TempDir message) failed with NoClassDefFoundError: org/slf4j/spi/LocationAwareLogger. --- .../embedded/EmbeddedMavenExecutor.java | 39 ++++++++++ .../EmbeddedMavenExecutorLoggingTest.java | 76 +++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 maven-executor/src/test/java/org/apache/maven/executor/embedded/EmbeddedMavenExecutorLoggingTest.java diff --git a/maven-executor/src/main/java/org/apache/maven/executor/embedded/EmbeddedMavenExecutor.java b/maven-executor/src/main/java/org/apache/maven/executor/embedded/EmbeddedMavenExecutor.java index eef1a04..65ca582 100644 --- a/maven-executor/src/main/java/org/apache/maven/executor/embedded/EmbeddedMavenExecutor.java +++ b/maven-executor/src/main/java/org/apache/maven/executor/embedded/EmbeddedMavenExecutor.java @@ -44,6 +44,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; +import java.util.logging.Handler; +import java.util.logging.Logger; import java.util.stream.Stream; import org.apache.maven.executor.Executor; @@ -430,6 +432,7 @@ protected void doClose(Context context) throws ExecutorException { Thread.currentThread().setContextClassLoader(context.bootClassLoader); try { try { + removeJulHandlersLoadedByMaven(context); ((Closeable) context.classWorld).close(); } finally { context.bootClassLoader.close(); @@ -441,6 +444,42 @@ protected void doClose(Context context) throws ExecutorException { } } + /** + * Maven may install a {@code java.util.logging} handler (like the SLF4J bridge) on the root logger. The root logger + * is JVM-global, while the handler class belongs to a Maven realm, so once the realm is closed the handler fails + * with {@link NoClassDefFoundError} on the first record logged by anyone in this JVM. Hence, uninstall them. + */ + private void removeJulHandlersLoadedByMaven(Context context) { + Logger root = Logger.getLogger(""); + for (Handler handler : root.getHandlers()) { + if (isLoadedByMaven(handler.getClass().getClassLoader(), context)) { + root.removeHandler(handler); + try { + handler.close(); + } catch (RuntimeException | LinkageError e) { + // best effort: the handler is already detached + } + } + } + } + + private boolean isLoadedByMaven(ClassLoader classLoader, Context context) { + if (classLoader == null) { + return false; + } + if (classLoader == context.bootClassLoader) { + return true; + } + if (classLoader.getClass().getName().equals("org.codehaus.plexus.classworlds.realm.ClassRealm")) { + try { + return classLoader.getClass().getMethod("getWorld").invoke(classLoader) == context.classWorld; + } catch (ReflectiveOperationException e) { + return false; + } + } + return false; + } + protected void validate(ExecutorRequest executorRequest) throws ExecutorException {} protected URLClassLoader createMavenBootClassLoader(Path boot, List extraClasspath) { diff --git a/maven-executor/src/test/java/org/apache/maven/executor/embedded/EmbeddedMavenExecutorLoggingTest.java b/maven-executor/src/test/java/org/apache/maven/executor/embedded/EmbeddedMavenExecutorLoggingTest.java new file mode 100644 index 0000000..a8eaf56 --- /dev/null +++ b/maven-executor/src/test/java/org/apache/maven/executor/embedded/EmbeddedMavenExecutorLoggingTest.java @@ -0,0 +1,76 @@ +/* + * 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.executor.embedded; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Handler; +import java.util.logging.Logger; + +import org.apache.maven.executor.Environment; +import org.apache.maven.executor.ExecutorRequest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Embedded Maven runs in this JVM, but {@code java.util.logging} is JVM-global. A handler that Maven installs on the + * root logger comes from a Maven class realm, and must not outlive the executor that closed that realm. + */ +@Timeout(120) +public class EmbeddedMavenExecutorLoggingTest { + @TempDir + private Path tempDir; + + @Test + void closeRemovesJulHandlersInstalledByMaven3() throws Exception { + assertCloseLeavesRootHandlersAlone(Paths.get(Environment.MAVEN3_HOME)); + } + + @Test + void closeRemovesJulHandlersInstalledByMaven4() throws Exception { + assertCloseLeavesRootHandlersAlone(Paths.get(Environment.MAVEN4_HOME)); + } + + private void assertCloseLeavesRootHandlersAlone(Path mavenHome) throws Exception { + Path cwd = + Files.createDirectories(tempDir.resolve("cwd").resolve(".mvn")).getParent(); + Path userHome = Files.createDirectories(tempDir.resolve("home")); + Logger root = Logger.getLogger(""); + Set before = new HashSet<>(Arrays.asList(root.getHandlers())); + try (EmbeddedMavenExecutor executor = new EmbeddedMavenExecutor(mavenHome)) { + executor.execute(ExecutorRequest.mavenBuilder() + .cwd(cwd) + .userHomeDirectory(userHome) + .argument("--version") + .build()); + } finally { + Set leaked = new HashSet<>(Arrays.asList(root.getHandlers())); + leaked.removeAll(before); + leaked.forEach(root::removeHandler); // keep this test from poisoning the rest of the JVM + assertEquals(new HashSet(), leaked, "Handlers left on the JUL root logger"); + } + } +}