From 19f9a173fc79135b46b4169d37d56d0486f39952 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Fri, 18 Sep 2026 15:36:01 +0800 Subject: [PATCH 01/13] feat(mockserver): bridge JUL to SLF4J for MockWebServer logs okhttp's MockWebServer logs via java.util.logging directly, so its output bypassed slf4j/logback even with jul-to-slf4j on the classpath. Install the bridge in ApolloTestingServer's static initializer so mock server logs are routed consistently with the rest of the test output. Co-Authored-By: Claude Sonnet 5 --- apollo-mockserver/pom.xml | 4 ++++ .../framework/apollo/mockserver/ApolloTestingServer.java | 6 ++++++ pom.xml | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/apollo-mockserver/pom.xml b/apollo-mockserver/pom.xml index 9dec2d50..15dbf32a 100644 --- a/apollo-mockserver/pom.xml +++ b/apollo-mockserver/pom.xml @@ -53,6 +53,10 @@ com.ctrip.framework.apollo apollo-client + + org.slf4j + jul-to-slf4j + com.squareup.okhttp3 mockwebserver diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java index e69acc45..86e22fb6 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java @@ -37,6 +37,7 @@ import okhttp3.mockwebserver.RecordedRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.slf4j.bridge.SLF4JBridgeHandler; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -80,6 +81,11 @@ public class ApolloTestingServer implements AutoCloseable { private boolean closed; static { + // okhttp's MockWebServer logs via java.util.logging directly; having jul-to-slf4j on the + // classpath alone doesn't route it, the JUL root handler must be removed and the bridge + // installed explicitly so these logs end up in slf4j/logback like everything else. + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); try { System.setProperty("apollo.longPollingInitialDelayInMills", "0"); CONFIG_SERVICE_LOCATOR = ApolloInjector.getInstance(ConfigServiceLocator.class); diff --git a/pom.xml b/pom.xml index a20f7096..bd64c6f1 100644 --- a/pom.xml +++ b/pom.xml @@ -111,6 +111,11 @@ slf4j-api 1.7.21 + + org.slf4j + jul-to-slf4j + 1.7.21 + com.google.guava guava From 36229b8591d1d3b756309edd92271c1972665b14 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Fri, 18 Sep 2026 15:56:07 +0800 Subject: [PATCH 02/13] fix(mockserver): scope JUL bridge install to server start/close The bridge was installed in ApolloTestingServer's static initializer, which strips the JVM's root JUL handlers as soon as the class loads and never restores them, silently altering logging for any other code sharing the JVM. Move installation into start()/close() instead, and add a reference-counted JulSlf4jBridge helper that restores the original root handlers once the last active mock server closes. Co-Authored-By: Claude Sonnet 5 --- .../mockserver/ApolloTestingServer.java | 8 +-- .../apollo/mockserver/JulSlf4jBridge.java | 54 +++++++++++++++++++ 2 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java index 86e22fb6..31ae0537 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java @@ -37,7 +37,6 @@ import okhttp3.mockwebserver.RecordedRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.bridge.SLF4JBridgeHandler; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -81,11 +80,6 @@ public class ApolloTestingServer implements AutoCloseable { private boolean closed; static { - // okhttp's MockWebServer logs via java.util.logging directly; having jul-to-slf4j on the - // classpath alone doesn't route it, the JUL root handler must be removed and the bridge - // installed explicitly so these logs end up in slf4j/logback like everything else. - SLF4JBridgeHandler.removeHandlersForRootLogger(); - SLF4JBridgeHandler.install(); try { System.setProperty("apollo.longPollingInitialDelayInMills", "0"); CONFIG_SERVICE_LOCATOR = ApolloInjector.getInstance(ConfigServiceLocator.class); @@ -110,6 +104,7 @@ public class ApolloTestingServer implements AutoCloseable { } public void start() throws IOException { + JulSlf4jBridge.install(); clearForStart(); server = new MockWebServer(); final Dispatcher dispatcher = new Dispatcher() { @@ -146,6 +141,7 @@ public void close() { logger.error("stop apollo server error", e); } finally { closed = true; + JulSlf4jBridge.uninstall(); } } diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java new file mode 100644 index 00000000..7586d4f4 --- /dev/null +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Apollo Authors + * + * Licensed 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 com.ctrip.framework.apollo.mockserver; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Handler; +import java.util.logging.LogManager; +import org.slf4j.bridge.SLF4JBridgeHandler; + +/** + * Routes okhttp's MockWebServer logs (which go through java.util.logging directly) to + * slf4j/logback for the lifetime of active {@link ApolloTestingServer} instances, restoring the + * original JUL root handlers once the last one closes. Reference-counted so concurrently running + * mock servers don't uninstall the bridge out from under each other. + */ +final class JulSlf4jBridge { + + private static final AtomicInteger ACTIVE = new AtomicInteger(0); + private static Handler[] originalHandlers; + + private JulSlf4jBridge() {} + + static synchronized void install() { + if (ACTIVE.getAndIncrement() == 0) { + originalHandlers = LogManager.getLogManager().getLogger("").getHandlers(); + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + } + } + + static synchronized void uninstall() { + if (ACTIVE.decrementAndGet() == 0) { + SLF4JBridgeHandler.uninstall(); + for (Handler handler : originalHandlers) { + LogManager.getLogManager().getLogger("").addHandler(handler); + } + originalHandlers = null; + } + } +} From a55c66e90ea8e9546c130cd946a81e4dafaadf63 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Sun, 20 Sep 2026 14:20:38 +0800 Subject: [PATCH 03/13] fix(mockserver): address JUL bridge review findings Three issues from review: - Bridging unconditionally onto slf4j-jdk14 creates an infinite JUL<->SLF4J loop, so skip installation when that binding is detected. - The bridge's install/uninstall calls were only reference-counted globally; closing the same ApolloTestingServer twice, or closing an instance that was never started, could under/over-decrement the shared counter and disrupt a still-running server. Add a per-instance acquire/release guard so each server's contribution is counted exactly once, and roll back on a failed start() instead of leaving the bridge acquired. - Scope the bridge to MockWebServer's own JUL logger instead of the JUL root logger, so unrelated JUL handlers elsewhere in the JVM are never touched, not just restored afterward. Adds JulSlf4jBridgeTest covering the binding check and the two lifecycle-accounting repro cases called out in review. Co-Authored-By: Claude Sonnet 5 --- .../mockserver/ApolloTestingServer.java | 69 +++++++---- .../apollo/mockserver/JulSlf4jBridge.java | 64 +++++++--- .../apollo/mockserver/JulSlf4jBridgeTest.java | 117 ++++++++++++++++++ 3 files changed, 212 insertions(+), 38 deletions(-) create mode 100644 apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java index 31ae0537..a2f30ae2 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java @@ -79,6 +79,8 @@ public class ApolloTestingServer implements AutoCloseable { private boolean closed; + private boolean bridgeAcquired; + static { try { System.setProperty("apollo.longPollingInitialDelayInMills", "0"); @@ -104,33 +106,38 @@ public class ApolloTestingServer implements AutoCloseable { } public void start() throws IOException { - JulSlf4jBridge.install(); - clearForStart(); - server = new MockWebServer(); - final Dispatcher dispatcher = new Dispatcher() { - @Override - public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().startsWith("/notifications/v2")) { - String notifications = request.getRequestUrl().queryParameter("notifications"); - return new MockResponse().setResponseCode(200).setBody(mockLongPollBody(notifications)); - } - if (request.getPath().startsWith("/configs")) { - List pathSegments = request.getRequestUrl().pathSegments(); - // appId and cluster might be used in the future - String appId = pathSegments.get(1); - String cluster = pathSegments.get(2); - String namespace = pathSegments.get(3); - return new MockResponse().setResponseCode(200).setBody(loadConfigFor(appId, namespace)); + acquireJulBridge(); + try { + clearForStart(); + server = new MockWebServer(); + final Dispatcher dispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + if (request.getPath().startsWith("/notifications/v2")) { + String notifications = request.getRequestUrl().queryParameter("notifications"); + return new MockResponse().setResponseCode(200).setBody(mockLongPollBody(notifications)); + } + if (request.getPath().startsWith("/configs")) { + List pathSegments = request.getRequestUrl().pathSegments(); + // appId and cluster might be used in the future + String appId = pathSegments.get(1); + String cluster = pathSegments.get(2); + String namespace = pathSegments.get(3); + return new MockResponse().setResponseCode(200).setBody(loadConfigFor(appId, namespace)); + } + return new MockResponse().setResponseCode(404); } - return new MockResponse().setResponseCode(404); - } - }; + }; - server.setDispatcher(dispatcher); - server.start(); + server.setDispatcher(dispatcher); + server.start(); - mockConfigServiceUrl("http://localhost:" + server.getPort()); - started = true; + mockConfigServiceUrl("http://localhost:" + server.getPort()); + started = true; + } catch (RuntimeException | IOException e) { + releaseJulBridge(); + throw e; + } } public void close() { @@ -141,7 +148,21 @@ public void close() { logger.error("stop apollo server error", e); } finally { closed = true; + releaseJulBridge(); + } + } + + private void acquireJulBridge() { + if (!bridgeAcquired) { + JulSlf4jBridge.install(); + bridgeAcquired = true; + } + } + + private void releaseJulBridge() { + if (bridgeAcquired) { JulSlf4jBridge.uninstall(); + bridgeAcquired = false; } } diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java index 7586d4f4..1b856d98 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java @@ -18,37 +18,73 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Handler; -import java.util.logging.LogManager; +import java.util.logging.Logger; +import org.slf4j.LoggerFactory; import org.slf4j.bridge.SLF4JBridgeHandler; /** * Routes okhttp's MockWebServer logs (which go through java.util.logging directly) to - * slf4j/logback for the lifetime of active {@link ApolloTestingServer} instances, restoring the - * original JUL root handlers once the last one closes. Reference-counted so concurrently running - * mock servers don't uninstall the bridge out from under each other. + * slf4j/logback for the lifetime of active {@link ApolloTestingServer} instances. + * + *

Unlike {@link SLF4JBridgeHandler#install()}, this only touches the JUL logger used by + * MockWebServer, not the JUL root logger, so unrelated JUL handlers elsewhere in the JVM are + * never removed. It also skips installing when the SLF4J binding is slf4j-jdk14, since bridging + * JUL to an SLF4J binding that itself delegates back to JUL creates an infinite loop. Calls are + * reference-counted so concurrently running mock servers don't uninstall the bridge out from + * under each other. */ final class JulSlf4jBridge { + private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(JulSlf4jBridge.class); + private static final String MOCK_WEB_SERVER_LOGGER_NAME = "okhttp3.mockwebserver.MockWebServer"; private static final AtomicInteger ACTIVE = new AtomicInteger(0); + private static Handler[] originalHandlers; + private static boolean originalUseParentHandlers; + private static boolean bridged; private JulSlf4jBridge() {} static synchronized void install() { - if (ACTIVE.getAndIncrement() == 0) { - originalHandlers = LogManager.getLogManager().getLogger("").getHandlers(); - SLF4JBridgeHandler.removeHandlersForRootLogger(); - SLF4JBridgeHandler.install(); + if (ACTIVE.getAndIncrement() > 0) { + return; + } + bridged = false; + if (isJdk14Binding(LoggerFactory.getILoggerFactory().getClass().getName())) { + LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because the SLF4J binding " + + "is slf4j-jdk14; bridging it would create an infinite JUL<->SLF4J logging loop."); + return; + } + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + originalHandlers = mockWebServerLogger.getHandlers(); + originalUseParentHandlers = mockWebServerLogger.getUseParentHandlers(); + for (Handler handler : originalHandlers) { + mockWebServerLogger.removeHandler(handler); } + mockWebServerLogger.addHandler(new SLF4JBridgeHandler()); + mockWebServerLogger.setUseParentHandlers(false); + bridged = true; } static synchronized void uninstall() { - if (ACTIVE.decrementAndGet() == 0) { - SLF4JBridgeHandler.uninstall(); - for (Handler handler : originalHandlers) { - LogManager.getLogManager().getLogger("").addHandler(handler); - } - originalHandlers = null; + if (ACTIVE.decrementAndGet() > 0 || !bridged) { + return; } + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + for (Handler handler : mockWebServerLogger.getHandlers()) { + mockWebServerLogger.removeHandler(handler); + } + for (Handler handler : originalHandlers) { + mockWebServerLogger.addHandler(handler); + } + mockWebServerLogger.setUseParentHandlers(originalUseParentHandlers); + originalHandlers = null; + bridged = false; + } + + /** Package-visible for testing without depending on the actual runtime SLF4J binding. */ + static boolean isJdk14Binding(String iLoggerFactoryClassName) { + return "org.slf4j.impl.JDK14LoggerFactory".equals(iLoggerFactoryClassName) + || "org.slf4j.jul.JDK14LoggerFactory".equals(iLoggerFactoryClassName); } } diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java new file mode 100644 index 00000000..2edaa024 --- /dev/null +++ b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java @@ -0,0 +1,117 @@ +/* + * Copyright 2026 Apollo Authors + * + * Licensed 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 com.ctrip.framework.apollo.mockserver; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.logging.Handler; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.slf4j.bridge.SLF4JBridgeHandler; + +class JulSlf4jBridgeTest { + + private static final String MOCK_WEB_SERVER_LOGGER_NAME = "okhttp3.mockwebserver.MockWebServer"; + + @Test + void isJdk14BindingDetectsKnownFactoryClassNames() { + assertTrue(JulSlf4jBridge.isJdk14Binding("org.slf4j.impl.JDK14LoggerFactory")); + assertTrue(JulSlf4jBridge.isJdk14Binding("org.slf4j.jul.JDK14LoggerFactory")); + assertFalse(JulSlf4jBridge.isJdk14Binding("ch.qos.logback.classic.LoggerContext")); + } + + @Test + void installOnlyTouchesMockWebServerLoggerNotRoot() { + Logger root = Logger.getLogger(""); + Handler[] rootHandlersBefore = root.getHandlers(); + + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + Handler[] mwsHandlersBefore = mockWebServerLogger.getHandlers(); + boolean useParentBefore = mockWebServerLogger.getUseParentHandlers(); + + JulSlf4jBridge.install(); + try { + assertArrayEquals(rootHandlersBefore, root.getHandlers(), + "installing the bridge must not touch the JUL root logger's handlers"); + assertTrue(hasBridgeHandler(mockWebServerLogger), + "MockWebServer's own logger should get the bridge handler"); + assertFalse(mockWebServerLogger.getUseParentHandlers()); + } finally { + JulSlf4jBridge.uninstall(); + } + + assertArrayEquals(rootHandlersBefore, root.getHandlers()); + assertArrayEquals(mwsHandlersBefore, mockWebServerLogger.getHandlers()); + assertEquals(useParentBefore, mockWebServerLogger.getUseParentHandlers()); + } + + @Test + void closingSameServerTwiceDoesNotDisruptAnotherActiveServer() throws Exception { + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + ApolloTestingServer serverA = new ApolloTestingServer(); + ApolloTestingServer serverB = new ApolloTestingServer(); + try { + serverA.start(); + serverB.start(); + assertTrue(hasBridgeHandler(mockWebServerLogger)); + + serverA.close(); + serverA.close(); + + assertTrue(hasBridgeHandler(mockWebServerLogger), + "bridge must remain installed while server B is still running"); + } finally { + serverA.close(); + serverB.close(); + } + + assertFalse(hasBridgeHandler(mockWebServerLogger)); + } + + @Test + void closingNeverStartedServerDoesNotDisruptActiveServer() throws Exception { + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + ApolloTestingServer active = new ApolloTestingServer(); + ApolloTestingServer neverStarted = new ApolloTestingServer(); + try { + active.start(); + assertTrue(hasBridgeHandler(mockWebServerLogger)); + + neverStarted.close(); + + assertTrue(hasBridgeHandler(mockWebServerLogger), + "closing a server that was never started must not release a bridge it never acquired"); + } finally { + active.close(); + neverStarted.close(); + } + + assertFalse(hasBridgeHandler(mockWebServerLogger)); + } + + private static boolean hasBridgeHandler(Logger logger) { + for (Handler handler : logger.getHandlers()) { + if (handler instanceof SLF4JBridgeHandler) { + return true; + } + } + return false; + } +} From d896f2b5f698c0c32de7a1aec356c27e493bf98a Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Mon, 21 Sep 2026 15:17:34 +0800 Subject: [PATCH 04/13] fix(mockserver): scope JUL bridge uninstall to its own handler uninstall() previously removed every handler on the MockWebServer JUL logger, which would drop any handler added by another component while the bridge was active. Track the SLF4JBridgeHandler instance added by install() and remove only that one, restoring original handlers only when not already present. Also add a test covering bridge release when ApolloTestingServer.start() fails after acquiring the bridge. Assisted-by: Claude Sonnet 5 --- .../apollo/mockserver/JulSlf4jBridge.java | 22 ++++++++++++++----- .../apollo/mockserver/JulSlf4jBridgeTest.java | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java index 1b856d98..9fc94f48 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java @@ -42,6 +42,7 @@ final class JulSlf4jBridge { private static Handler[] originalHandlers; private static boolean originalUseParentHandlers; private static boolean bridged; + private static SLF4JBridgeHandler bridgeHandler; private JulSlf4jBridge() {} @@ -61,7 +62,8 @@ static synchronized void install() { for (Handler handler : originalHandlers) { mockWebServerLogger.removeHandler(handler); } - mockWebServerLogger.addHandler(new SLF4JBridgeHandler()); + bridgeHandler = new SLF4JBridgeHandler(); + mockWebServerLogger.addHandler(bridgeHandler); mockWebServerLogger.setUseParentHandlers(false); bridged = true; } @@ -71,17 +73,27 @@ static synchronized void uninstall() { return; } Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); - for (Handler handler : mockWebServerLogger.getHandlers()) { - mockWebServerLogger.removeHandler(handler); - } + mockWebServerLogger.removeHandler(bridgeHandler); for (Handler handler : originalHandlers) { - mockWebServerLogger.addHandler(handler); + if (!containsHandler(mockWebServerLogger.getHandlers(), handler)) { + mockWebServerLogger.addHandler(handler); + } } mockWebServerLogger.setUseParentHandlers(originalUseParentHandlers); originalHandlers = null; + bridgeHandler = null; bridged = false; } + private static boolean containsHandler(Handler[] handlers, Handler target) { + for (Handler handler : handlers) { + if (handler == target) { + return true; + } + } + return false; + } + /** Package-visible for testing without depending on the actual runtime SLF4J binding. */ static boolean isJdk14Binding(String iLoggerFactoryClassName) { return "org.slf4j.impl.JDK14LoggerFactory".equals(iLoggerFactoryClassName) diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java index 2edaa024..bbce63f8 100644 --- a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java +++ b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java @@ -19,8 +19,10 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.reflect.Field; import java.util.logging.Handler; import java.util.logging.Logger; import org.junit.jupiter.api.Test; @@ -106,6 +108,26 @@ void closingNeverStartedServerDoesNotDisruptActiveServer() throws Exception { assertFalse(hasBridgeHandler(mockWebServerLogger)); } + @Test + void startupFailureAfterBridgeAcquisitionReleasesBridge() throws Exception { + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + ApolloTestingServer server = new ApolloTestingServer(); + + Field locatorField = ApolloTestingServer.class.getDeclaredField("CONFIG_SERVICE_LOCATOR"); + locatorField.setAccessible(true); + Object originalLocator = locatorField.get(null); + locatorField.set(null, null); + try { + assertThrows(IllegalStateException.class, server::start, + "start() must surface failures that happen after the JUL bridge is acquired"); + assertFalse(hasBridgeHandler(mockWebServerLogger), + "a failed start() must release the JUL bridge it acquired"); + } finally { + locatorField.set(null, originalLocator); + server.close(); + } + } + private static boolean hasBridgeHandler(Logger logger) { for (Handler handler : logger.getHandlers()) { if (handler instanceof SLF4JBridgeHandler) { From 8a27ad6c423bbc5578b4a0301f1572cd14ebc772 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Thu, 8 Oct 2026 17:53:16 +0800 Subject: [PATCH 05/13] fix(mockserver): retain JUL logger for bridge lifetime Keep the MockWebServer logger strongly reachable until bridge teardown so GC cannot silently detach its SLF4J handler during server startup. Assisted-by: Claude Sonnet 5 --- CHANGES.md | 1 + .../apollo/mockserver/JulSlf4jBridge.java | 5 +++-- .../apollo/mockserver/JulSlf4jBridgeTest.java | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c18c8d75..10e8d88f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Apollo Java 2.6.0 * [Fix Apollo client local cache fallback for Spring Boot 3 executable JARs](https://github.com/apolloconfig/apollo-java/pull/136) * [Support `extendInfo`/`parentAppId` in OpenAPI `getNamespace(s)` client calls](https://github.com/apolloconfig/apollo-java/pull/144) +* [Bridge MockWebServer JUL logs to SLF4J](https://github.com/apolloconfig/apollo-java/pull/147) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/6?closed=1) diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java index 9fc94f48..00571a56 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java @@ -42,6 +42,7 @@ final class JulSlf4jBridge { private static Handler[] originalHandlers; private static boolean originalUseParentHandlers; private static boolean bridged; + private static Logger mockWebServerLogger; private static SLF4JBridgeHandler bridgeHandler; private JulSlf4jBridge() {} @@ -56,7 +57,7 @@ static synchronized void install() { + "is slf4j-jdk14; bridging it would create an infinite JUL<->SLF4J logging loop."); return; } - Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); originalHandlers = mockWebServerLogger.getHandlers(); originalUseParentHandlers = mockWebServerLogger.getUseParentHandlers(); for (Handler handler : originalHandlers) { @@ -72,7 +73,6 @@ static synchronized void uninstall() { if (ACTIVE.decrementAndGet() > 0 || !bridged) { return; } - Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); mockWebServerLogger.removeHandler(bridgeHandler); for (Handler handler : originalHandlers) { if (!containsHandler(mockWebServerLogger.getHandlers(), handler)) { @@ -81,6 +81,7 @@ static synchronized void uninstall() { } mockWebServerLogger.setUseParentHandlers(originalUseParentHandlers); originalHandlers = null; + mockWebServerLogger = null; bridgeHandler = null; bridged = false; } diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java index bbce63f8..52c01fa5 100644 --- a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java +++ b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java @@ -108,6 +108,25 @@ void closingNeverStartedServerDoesNotDisruptActiveServer() throws Exception { assertFalse(hasBridgeHandler(mockWebServerLogger)); } + @Test + void bridgeRetainsLoggerUntilMockWebServerInitializes() throws Exception { + ApolloTestingServer server = new ApolloTestingServer() { + @Override + public void resetOverriddenProperties() { + System.gc(); + super.resetOverriddenProperties(); + } + }; + try { + server.start(); + + assertTrue(hasBridgeHandler(Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME)), + "the MockWebServer logger must retain its bridge across GC during startup"); + } finally { + server.close(); + } + } + @Test void startupFailureAfterBridgeAcquisitionReleasesBridge() throws Exception { Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); From 674bc10cc2be05a24d51baa8737b795a355b9312 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Mon, 12 Oct 2026 16:47:16 +0800 Subject: [PATCH 06/13] fix(mockserver): install JUL bridge after MockWebServer is fully started Previously the bridge was installed at the beginning of start(), which could interfere with MockWebServer initialization on certain JDK 8 runtimes. Moving it to after the server is running and the config service URL is set avoids this issue. Assisted-by: Claude Sonnet 5 --- .../framework/apollo/mockserver/ApolloTestingServer.java | 9 +++++---- .../framework/apollo/mockserver/JulSlf4jBridgeTest.java | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java index a2f30ae2..f24baea8 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java @@ -106,7 +106,6 @@ public class ApolloTestingServer implements AutoCloseable { } public void start() throws IOException { - acquireJulBridge(); try { clearForStart(); server = new MockWebServer(); @@ -134,10 +133,12 @@ public MockResponse dispatch(RecordedRequest request) throws InterruptedExceptio mockConfigServiceUrl("http://localhost:" + server.getPort()); started = true; - } catch (RuntimeException | IOException e) { - releaseJulBridge(); - throw e; + } finally { + if (!started) { + releaseJulBridge(); + } } + acquireJulBridge(); } public void close() { diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java index 52c01fa5..2aeb34e3 100644 --- a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java +++ b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java @@ -138,9 +138,9 @@ void startupFailureAfterBridgeAcquisitionReleasesBridge() throws Exception { locatorField.set(null, null); try { assertThrows(IllegalStateException.class, server::start, - "start() must surface failures that happen after the JUL bridge is acquired"); + "start() must surface failures"); assertFalse(hasBridgeHandler(mockWebServerLogger), - "a failed start() must release the JUL bridge it acquired"); + "a failed start() must not install the JUL bridge"); } finally { locatorField.set(null, originalLocator); server.close(); From a68195f1fc99182622f8e01e649f9851854bc666 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Mon, 12 Oct 2026 18:07:26 +0800 Subject: [PATCH 07/13] fix(mockserver): make jul-to-slf4j optional and add graceful fallback Mark jul-to-slf4j as optional in mockserver's pom so it does not leak as a transitive dependency to downstream modules. Guard SLF4JBridgeHandler instantiation with NoClassDefFoundError recovery to gracefully degrade when the jar is absent at runtime. Assisted-by: Claude Sonnet 5 --- apollo-mockserver/pom.xml | 1 + .../apollo/mockserver/JulSlf4jBridge.java | 14 +++++++++++++- .../apollo/mockserver/JulSlf4jBridgeTest.java | 3 +-- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/apollo-mockserver/pom.xml b/apollo-mockserver/pom.xml index 15dbf32a..4cfb3f87 100644 --- a/apollo-mockserver/pom.xml +++ b/apollo-mockserver/pom.xml @@ -56,6 +56,7 @@ org.slf4j jul-to-slf4j + true com.squareup.okhttp3 diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java index 00571a56..39552343 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java @@ -63,7 +63,19 @@ static synchronized void install() { for (Handler handler : originalHandlers) { mockWebServerLogger.removeHandler(handler); } - bridgeHandler = new SLF4JBridgeHandler(); + try { + bridgeHandler = new SLF4JBridgeHandler(); + } catch (NoClassDefFoundError e) { + LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because " + + "jul-to-slf4j is not on the classpath"); + mockWebServerLogger.setUseParentHandlers(originalUseParentHandlers); + for (Handler handler : originalHandlers) { + mockWebServerLogger.addHandler(handler); + } + originalHandlers = null; + mockWebServerLogger = null; + return; + } mockWebServerLogger.addHandler(bridgeHandler); mockWebServerLogger.setUseParentHandlers(false); bridged = true; diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java index 2aeb34e3..296021fc 100644 --- a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java +++ b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java @@ -137,8 +137,7 @@ void startupFailureAfterBridgeAcquisitionReleasesBridge() throws Exception { Object originalLocator = locatorField.get(null); locatorField.set(null, null); try { - assertThrows(IllegalStateException.class, server::start, - "start() must surface failures"); + assertThrows(IllegalStateException.class, server::start, "start() must surface failures"); assertFalse(hasBridgeHandler(mockWebServerLogger), "a failed start() must not install the JUL bridge"); } finally { From 24682805e0e933b9b14aba4d7e07ebf1efd619d2 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Mon, 12 Oct 2026 19:07:48 +0800 Subject: [PATCH 08/13] fix(mockserver): use reflection for SLF4JBridgeHandler to avoid class-loading dependency Replace direct import of SLF4JBridgeHandler with Class.forName() reflection so the JulSlf4jBridge class can load even when jul-to-slf4j is absent from the classpath. Mark jul-to-slf4j as optional in mockserver's pom to prevent it from leaking as a transitive dependency to downstream modules. Assisted-by: Claude Sonnet 5 --- .../apollo/mockserver/JulSlf4jBridge.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java index 39552343..8e01c286 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java @@ -20,30 +20,33 @@ import java.util.logging.Handler; import java.util.logging.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.bridge.SLF4JBridgeHandler; /** * Routes okhttp's MockWebServer logs (which go through java.util.logging directly) to * slf4j/logback for the lifetime of active {@link ApolloTestingServer} instances. * - *

Unlike {@link SLF4JBridgeHandler#install()}, this only touches the JUL logger used by + *

Unlike {@code SLF4JBridgeHandler#install()}, this only touches the JUL logger used by * MockWebServer, not the JUL root logger, so unrelated JUL handlers elsewhere in the JVM are * never removed. It also skips installing when the SLF4J binding is slf4j-jdk14, since bridging * JUL to an SLF4J binding that itself delegates back to JUL creates an infinite loop. Calls are * reference-counted so concurrently running mock servers don't uninstall the bridge out from * under each other. + * + *

Uses reflection to instantiate {@code SLF4JBridgeHandler} so the class can load even when + * {@code jul-to-slf4j} is not on the classpath. */ final class JulSlf4jBridge { private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(JulSlf4jBridge.class); private static final String MOCK_WEB_SERVER_LOGGER_NAME = "okhttp3.mockwebserver.MockWebServer"; + private static final String SLF4J_BRIDGE_HANDLER_CLASS = "org.slf4j.bridge.SLF4JBridgeHandler"; private static final AtomicInteger ACTIVE = new AtomicInteger(0); private static Handler[] originalHandlers; private static boolean originalUseParentHandlers; private static boolean bridged; private static Logger mockWebServerLogger; - private static SLF4JBridgeHandler bridgeHandler; + private static Handler bridgeHandler; private JulSlf4jBridge() {} @@ -64,10 +67,10 @@ static synchronized void install() { mockWebServerLogger.removeHandler(handler); } try { - bridgeHandler = new SLF4JBridgeHandler(); - } catch (NoClassDefFoundError e) { + bridgeHandler = (Handler) Class.forName(SLF4J_BRIDGE_HANDLER_CLASS).newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because " - + "jul-to-slf4j is not on the classpath"); + + "jul-to-slf4j is not on the classpath", e); mockWebServerLogger.setUseParentHandlers(originalUseParentHandlers); for (Handler handler : originalHandlers) { mockWebServerLogger.addHandler(handler); From e3e4af4e150dc6f4c38917df5cb60fae6873ee35 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Mon, 12 Oct 2026 20:14:40 +0800 Subject: [PATCH 09/13] fix(mockserver): check SLF4JBridgeHandler availability before touching JUL Move the Class.forName check for SLF4JBridgeHandler to the very beginning of install(), before any JUL logger operations. When jul-to-slf4j is absent from the classpath, the method returns immediately without initializing the JUL LogManager, avoiding interference with MockWebServer on JDK 8 CI runners. Assisted-by: Claude Sonnet 5 --- .../apollo/mockserver/JulSlf4jBridge.java | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java index 8e01c286..b064d57d 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java @@ -55,6 +55,11 @@ static synchronized void install() { return; } bridged = false; + if (!isBridgeHandlerAvailable()) { + LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because " + + "jul-to-slf4j is not on the classpath"); + return; + } if (isJdk14Binding(LoggerFactory.getILoggerFactory().getClass().getName())) { LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because the SLF4J binding " + "is slf4j-jdk14; bridging it would create an infinite JUL<->SLF4J logging loop."); @@ -66,24 +71,30 @@ static synchronized void install() { for (Handler handler : originalHandlers) { mockWebServerLogger.removeHandler(handler); } - try { - bridgeHandler = (Handler) Class.forName(SLF4J_BRIDGE_HANDLER_CLASS).newInstance(); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because " - + "jul-to-slf4j is not on the classpath", e); - mockWebServerLogger.setUseParentHandlers(originalUseParentHandlers); - for (Handler handler : originalHandlers) { - mockWebServerLogger.addHandler(handler); - } - originalHandlers = null; - mockWebServerLogger = null; - return; - } + bridgeHandler = newBridgeHandler(); mockWebServerLogger.addHandler(bridgeHandler); mockWebServerLogger.setUseParentHandlers(false); bridged = true; } + private static boolean isBridgeHandlerAvailable() { + try { + Class.forName(SLF4J_BRIDGE_HANDLER_CLASS); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + private static Handler newBridgeHandler() { + try { + return (Handler) Class.forName(SLF4J_BRIDGE_HANDLER_CLASS).newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new IllegalStateException( + "SLF4JBridgeHandler was available during check but not during instantiation", e); + } + } + static synchronized void uninstall() { if (ACTIVE.decrementAndGet() > 0 || !bridged) { return; From dc195b08097a2051755d971d05372e4f014d0b80 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Mon, 12 Oct 2026 20:35:52 +0800 Subject: [PATCH 10/13] fix(mockserver): make JUL bridge installation explicit and opt-in Remove automatic bridge install/release from ApolloTestingServer.start() and close(). Instead, expose acquireJulBridge() and releaseJulBridge() as public methods so callers opt in explicitly. This avoids any side-effect on downstream tests that use ApolloTestingServer without needing the bridge. Assisted-by: Claude Sonnet 5 --- .../mockserver/ApolloTestingServer.java | 65 +++++++++---------- .../apollo/mockserver/JulSlf4jBridgeTest.java | 19 ++++-- 2 files changed, 45 insertions(+), 39 deletions(-) diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java index f24baea8..c7e98aa3 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java @@ -106,39 +106,32 @@ public class ApolloTestingServer implements AutoCloseable { } public void start() throws IOException { - try { - clearForStart(); - server = new MockWebServer(); - final Dispatcher dispatcher = new Dispatcher() { - @Override - public MockResponse dispatch(RecordedRequest request) throws InterruptedException { - if (request.getPath().startsWith("/notifications/v2")) { - String notifications = request.getRequestUrl().queryParameter("notifications"); - return new MockResponse().setResponseCode(200).setBody(mockLongPollBody(notifications)); - } - if (request.getPath().startsWith("/configs")) { - List pathSegments = request.getRequestUrl().pathSegments(); - // appId and cluster might be used in the future - String appId = pathSegments.get(1); - String cluster = pathSegments.get(2); - String namespace = pathSegments.get(3); - return new MockResponse().setResponseCode(200).setBody(loadConfigFor(appId, namespace)); - } - return new MockResponse().setResponseCode(404); + clearForStart(); + server = new MockWebServer(); + final Dispatcher dispatcher = new Dispatcher() { + @Override + public MockResponse dispatch(RecordedRequest request) throws InterruptedException { + if (request.getPath().startsWith("/notifications/v2")) { + String notifications = request.getRequestUrl().queryParameter("notifications"); + return new MockResponse().setResponseCode(200).setBody(mockLongPollBody(notifications)); + } + if (request.getPath().startsWith("/configs")) { + List pathSegments = request.getRequestUrl().pathSegments(); + // appId and cluster might be used in the future + String appId = pathSegments.get(1); + String cluster = pathSegments.get(2); + String namespace = pathSegments.get(3); + return new MockResponse().setResponseCode(200).setBody(loadConfigFor(appId, namespace)); } - }; + return new MockResponse().setResponseCode(404); + } + }; - server.setDispatcher(dispatcher); - server.start(); + server.setDispatcher(dispatcher); + server.start(); - mockConfigServiceUrl("http://localhost:" + server.getPort()); - started = true; - } finally { - if (!started) { - releaseJulBridge(); - } - } - acquireJulBridge(); + mockConfigServiceUrl("http://localhost:" + server.getPort()); + started = true; } public void close() { @@ -149,18 +142,24 @@ public void close() { logger.error("stop apollo server error", e); } finally { closed = true; - releaseJulBridge(); } } - private void acquireJulBridge() { + /** + * Installs the JUL-to-SLF4J bridge for MockWebServer logs on the currently active server. + * Callers must pair every call with {@link #releaseJulBridge()}. + */ + public void acquireJulBridge() { if (!bridgeAcquired) { JulSlf4jBridge.install(); bridgeAcquired = true; } } - private void releaseJulBridge() { + /** + * Releases the JUL-to-SLF4J bridge acquired by {@link #acquireJulBridge()}. + */ + public void releaseJulBridge() { if (bridgeAcquired) { JulSlf4jBridge.uninstall(); bridgeAcquired = false; diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java index 296021fc..482cb8f7 100644 --- a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java +++ b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java @@ -71,16 +71,20 @@ void closingSameServerTwiceDoesNotDisruptAnotherActiveServer() throws Exception ApolloTestingServer serverB = new ApolloTestingServer(); try { serverA.start(); + serverA.acquireJulBridge(); serverB.start(); + serverB.acquireJulBridge(); assertTrue(hasBridgeHandler(mockWebServerLogger)); - serverA.close(); - serverA.close(); + serverA.releaseJulBridge(); + serverA.releaseJulBridge(); assertTrue(hasBridgeHandler(mockWebServerLogger), "bridge must remain installed while server B is still running"); } finally { + serverA.releaseJulBridge(); serverA.close(); + serverB.releaseJulBridge(); serverB.close(); } @@ -94,13 +98,15 @@ void closingNeverStartedServerDoesNotDisruptActiveServer() throws Exception { ApolloTestingServer neverStarted = new ApolloTestingServer(); try { active.start(); + active.acquireJulBridge(); assertTrue(hasBridgeHandler(mockWebServerLogger)); - neverStarted.close(); + neverStarted.releaseJulBridge(); assertTrue(hasBridgeHandler(mockWebServerLogger), - "closing a server that was never started must not release a bridge it never acquired"); + "releasing a bridge that was never acquired must not affect the active bridge"); } finally { + active.releaseJulBridge(); active.close(); neverStarted.close(); } @@ -119,16 +125,17 @@ public void resetOverriddenProperties() { }; try { server.start(); - + server.acquireJulBridge(); assertTrue(hasBridgeHandler(Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME)), "the MockWebServer logger must retain its bridge across GC during startup"); } finally { + server.releaseJulBridge(); server.close(); } } @Test - void startupFailureAfterBridgeAcquisitionReleasesBridge() throws Exception { + void startupFailureDoesNotInstallBridge() throws Exception { Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); ApolloTestingServer server = new ApolloTestingServer(); From 9acb4706d8221d50a7ef78eb5af4dd52ddaee9b0 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Tue, 13 Oct 2026 10:34:16 +0800 Subject: [PATCH 11/13] chore(mockserver): revert root pom.xml to main, add version in mockserver only Move jul-to-slf4j version from root dependencyManagement into apollo-mockserver/pom.xml directly, keeping the root pom identical to main. Assisted-by: Claude Sonnet 5 --- apollo-mockserver/pom.xml | 1 + pom.xml | 5 ----- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/apollo-mockserver/pom.xml b/apollo-mockserver/pom.xml index 4cfb3f87..30bb0067 100644 --- a/apollo-mockserver/pom.xml +++ b/apollo-mockserver/pom.xml @@ -56,6 +56,7 @@ org.slf4j jul-to-slf4j + 1.7.21 true diff --git a/pom.xml b/pom.xml index bd64c6f1..a20f7096 100644 --- a/pom.xml +++ b/pom.xml @@ -111,11 +111,6 @@ slf4j-api 1.7.21 - - org.slf4j - jul-to-slf4j - 1.7.21 - com.google.guava guava From ec3c92e1289002d39d3c1a874e16daaec59abb31 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Tue, 13 Oct 2026 10:48:35 +0800 Subject: [PATCH 12/13] debug: revert all code to main to check if CI failure is pre-existing --- apollo-mockserver/pom.xml | 6 - .../mockserver/ApolloTestingServer.java | 23 --- .../apollo/mockserver/JulSlf4jBridge.java | 129 -------------- .../apollo/mockserver/JulSlf4jBridgeTest.java | 164 ------------------ 4 files changed, 322 deletions(-) delete mode 100644 apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java delete mode 100644 apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java diff --git a/apollo-mockserver/pom.xml b/apollo-mockserver/pom.xml index 30bb0067..9dec2d50 100644 --- a/apollo-mockserver/pom.xml +++ b/apollo-mockserver/pom.xml @@ -53,12 +53,6 @@ com.ctrip.framework.apollo apollo-client - - org.slf4j - jul-to-slf4j - 1.7.21 - true - com.squareup.okhttp3 mockwebserver diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java index c7e98aa3..e69acc45 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java @@ -79,8 +79,6 @@ public class ApolloTestingServer implements AutoCloseable { private boolean closed; - private boolean bridgeAcquired; - static { try { System.setProperty("apollo.longPollingInitialDelayInMills", "0"); @@ -145,27 +143,6 @@ public void close() { } } - /** - * Installs the JUL-to-SLF4J bridge for MockWebServer logs on the currently active server. - * Callers must pair every call with {@link #releaseJulBridge()}. - */ - public void acquireJulBridge() { - if (!bridgeAcquired) { - JulSlf4jBridge.install(); - bridgeAcquired = true; - } - } - - /** - * Releases the JUL-to-SLF4J bridge acquired by {@link #acquireJulBridge()}. - */ - public void releaseJulBridge() { - if (bridgeAcquired) { - JulSlf4jBridge.uninstall(); - bridgeAcquired = false; - } - } - public boolean isClosed() { return closed; } diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java deleted file mode 100644 index b064d57d..00000000 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2026 Apollo Authors - * - * Licensed 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 com.ctrip.framework.apollo.mockserver; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Handler; -import java.util.logging.Logger; -import org.slf4j.LoggerFactory; - -/** - * Routes okhttp's MockWebServer logs (which go through java.util.logging directly) to - * slf4j/logback for the lifetime of active {@link ApolloTestingServer} instances. - * - *

Unlike {@code SLF4JBridgeHandler#install()}, this only touches the JUL logger used by - * MockWebServer, not the JUL root logger, so unrelated JUL handlers elsewhere in the JVM are - * never removed. It also skips installing when the SLF4J binding is slf4j-jdk14, since bridging - * JUL to an SLF4J binding that itself delegates back to JUL creates an infinite loop. Calls are - * reference-counted so concurrently running mock servers don't uninstall the bridge out from - * under each other. - * - *

Uses reflection to instantiate {@code SLF4JBridgeHandler} so the class can load even when - * {@code jul-to-slf4j} is not on the classpath. - */ -final class JulSlf4jBridge { - - private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(JulSlf4jBridge.class); - private static final String MOCK_WEB_SERVER_LOGGER_NAME = "okhttp3.mockwebserver.MockWebServer"; - private static final String SLF4J_BRIDGE_HANDLER_CLASS = "org.slf4j.bridge.SLF4JBridgeHandler"; - private static final AtomicInteger ACTIVE = new AtomicInteger(0); - - private static Handler[] originalHandlers; - private static boolean originalUseParentHandlers; - private static boolean bridged; - private static Logger mockWebServerLogger; - private static Handler bridgeHandler; - - private JulSlf4jBridge() {} - - static synchronized void install() { - if (ACTIVE.getAndIncrement() > 0) { - return; - } - bridged = false; - if (!isBridgeHandlerAvailable()) { - LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because " - + "jul-to-slf4j is not on the classpath"); - return; - } - if (isJdk14Binding(LoggerFactory.getILoggerFactory().getClass().getName())) { - LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because the SLF4J binding " - + "is slf4j-jdk14; bridging it would create an infinite JUL<->SLF4J logging loop."); - return; - } - mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); - originalHandlers = mockWebServerLogger.getHandlers(); - originalUseParentHandlers = mockWebServerLogger.getUseParentHandlers(); - for (Handler handler : originalHandlers) { - mockWebServerLogger.removeHandler(handler); - } - bridgeHandler = newBridgeHandler(); - mockWebServerLogger.addHandler(bridgeHandler); - mockWebServerLogger.setUseParentHandlers(false); - bridged = true; - } - - private static boolean isBridgeHandlerAvailable() { - try { - Class.forName(SLF4J_BRIDGE_HANDLER_CLASS); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } - - private static Handler newBridgeHandler() { - try { - return (Handler) Class.forName(SLF4J_BRIDGE_HANDLER_CLASS).newInstance(); - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - throw new IllegalStateException( - "SLF4JBridgeHandler was available during check but not during instantiation", e); - } - } - - static synchronized void uninstall() { - if (ACTIVE.decrementAndGet() > 0 || !bridged) { - return; - } - mockWebServerLogger.removeHandler(bridgeHandler); - for (Handler handler : originalHandlers) { - if (!containsHandler(mockWebServerLogger.getHandlers(), handler)) { - mockWebServerLogger.addHandler(handler); - } - } - mockWebServerLogger.setUseParentHandlers(originalUseParentHandlers); - originalHandlers = null; - mockWebServerLogger = null; - bridgeHandler = null; - bridged = false; - } - - private static boolean containsHandler(Handler[] handlers, Handler target) { - for (Handler handler : handlers) { - if (handler == target) { - return true; - } - } - return false; - } - - /** Package-visible for testing without depending on the actual runtime SLF4J binding. */ - static boolean isJdk14Binding(String iLoggerFactoryClassName) { - return "org.slf4j.impl.JDK14LoggerFactory".equals(iLoggerFactoryClassName) - || "org.slf4j.jul.JDK14LoggerFactory".equals(iLoggerFactoryClassName); - } -} diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java deleted file mode 100644 index 482cb8f7..00000000 --- a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright 2026 Apollo Authors - * - * Licensed 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 com.ctrip.framework.apollo.mockserver; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.lang.reflect.Field; -import java.util.logging.Handler; -import java.util.logging.Logger; -import org.junit.jupiter.api.Test; -import org.slf4j.bridge.SLF4JBridgeHandler; - -class JulSlf4jBridgeTest { - - private static final String MOCK_WEB_SERVER_LOGGER_NAME = "okhttp3.mockwebserver.MockWebServer"; - - @Test - void isJdk14BindingDetectsKnownFactoryClassNames() { - assertTrue(JulSlf4jBridge.isJdk14Binding("org.slf4j.impl.JDK14LoggerFactory")); - assertTrue(JulSlf4jBridge.isJdk14Binding("org.slf4j.jul.JDK14LoggerFactory")); - assertFalse(JulSlf4jBridge.isJdk14Binding("ch.qos.logback.classic.LoggerContext")); - } - - @Test - void installOnlyTouchesMockWebServerLoggerNotRoot() { - Logger root = Logger.getLogger(""); - Handler[] rootHandlersBefore = root.getHandlers(); - - Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); - Handler[] mwsHandlersBefore = mockWebServerLogger.getHandlers(); - boolean useParentBefore = mockWebServerLogger.getUseParentHandlers(); - - JulSlf4jBridge.install(); - try { - assertArrayEquals(rootHandlersBefore, root.getHandlers(), - "installing the bridge must not touch the JUL root logger's handlers"); - assertTrue(hasBridgeHandler(mockWebServerLogger), - "MockWebServer's own logger should get the bridge handler"); - assertFalse(mockWebServerLogger.getUseParentHandlers()); - } finally { - JulSlf4jBridge.uninstall(); - } - - assertArrayEquals(rootHandlersBefore, root.getHandlers()); - assertArrayEquals(mwsHandlersBefore, mockWebServerLogger.getHandlers()); - assertEquals(useParentBefore, mockWebServerLogger.getUseParentHandlers()); - } - - @Test - void closingSameServerTwiceDoesNotDisruptAnotherActiveServer() throws Exception { - Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); - ApolloTestingServer serverA = new ApolloTestingServer(); - ApolloTestingServer serverB = new ApolloTestingServer(); - try { - serverA.start(); - serverA.acquireJulBridge(); - serverB.start(); - serverB.acquireJulBridge(); - assertTrue(hasBridgeHandler(mockWebServerLogger)); - - serverA.releaseJulBridge(); - serverA.releaseJulBridge(); - - assertTrue(hasBridgeHandler(mockWebServerLogger), - "bridge must remain installed while server B is still running"); - } finally { - serverA.releaseJulBridge(); - serverA.close(); - serverB.releaseJulBridge(); - serverB.close(); - } - - assertFalse(hasBridgeHandler(mockWebServerLogger)); - } - - @Test - void closingNeverStartedServerDoesNotDisruptActiveServer() throws Exception { - Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); - ApolloTestingServer active = new ApolloTestingServer(); - ApolloTestingServer neverStarted = new ApolloTestingServer(); - try { - active.start(); - active.acquireJulBridge(); - assertTrue(hasBridgeHandler(mockWebServerLogger)); - - neverStarted.releaseJulBridge(); - - assertTrue(hasBridgeHandler(mockWebServerLogger), - "releasing a bridge that was never acquired must not affect the active bridge"); - } finally { - active.releaseJulBridge(); - active.close(); - neverStarted.close(); - } - - assertFalse(hasBridgeHandler(mockWebServerLogger)); - } - - @Test - void bridgeRetainsLoggerUntilMockWebServerInitializes() throws Exception { - ApolloTestingServer server = new ApolloTestingServer() { - @Override - public void resetOverriddenProperties() { - System.gc(); - super.resetOverriddenProperties(); - } - }; - try { - server.start(); - server.acquireJulBridge(); - assertTrue(hasBridgeHandler(Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME)), - "the MockWebServer logger must retain its bridge across GC during startup"); - } finally { - server.releaseJulBridge(); - server.close(); - } - } - - @Test - void startupFailureDoesNotInstallBridge() throws Exception { - Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); - ApolloTestingServer server = new ApolloTestingServer(); - - Field locatorField = ApolloTestingServer.class.getDeclaredField("CONFIG_SERVICE_LOCATOR"); - locatorField.setAccessible(true); - Object originalLocator = locatorField.get(null); - locatorField.set(null, null); - try { - assertThrows(IllegalStateException.class, server::start, "start() must surface failures"); - assertFalse(hasBridgeHandler(mockWebServerLogger), - "a failed start() must not install the JUL bridge"); - } finally { - locatorField.set(null, originalLocator); - server.close(); - } - } - - private static boolean hasBridgeHandler(Logger logger) { - for (Handler handler : logger.getHandlers()) { - if (handler instanceof SLF4JBridgeHandler) { - return true; - } - } - return false; - } -} From 1dfb8b2bb75478ac7f64a0c09f070cb04d593ab2 Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Tue, 13 Oct 2026 10:56:59 +0800 Subject: [PATCH 13/13] Revert "debug: revert all code to main to check if CI failure is pre-existing" This reverts commit 053be47a54d894aecf9483f2f57f34c4ce9adcf4. --- apollo-mockserver/pom.xml | 6 + .../mockserver/ApolloTestingServer.java | 23 +++ .../apollo/mockserver/JulSlf4jBridge.java | 129 ++++++++++++++ .../apollo/mockserver/JulSlf4jBridgeTest.java | 164 ++++++++++++++++++ 4 files changed, 322 insertions(+) create mode 100644 apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java create mode 100644 apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java diff --git a/apollo-mockserver/pom.xml b/apollo-mockserver/pom.xml index 9dec2d50..30bb0067 100644 --- a/apollo-mockserver/pom.xml +++ b/apollo-mockserver/pom.xml @@ -53,6 +53,12 @@ com.ctrip.framework.apollo apollo-client + + org.slf4j + jul-to-slf4j + 1.7.21 + true + com.squareup.okhttp3 mockwebserver diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java index e69acc45..c7e98aa3 100644 --- a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/ApolloTestingServer.java @@ -79,6 +79,8 @@ public class ApolloTestingServer implements AutoCloseable { private boolean closed; + private boolean bridgeAcquired; + static { try { System.setProperty("apollo.longPollingInitialDelayInMills", "0"); @@ -143,6 +145,27 @@ public void close() { } } + /** + * Installs the JUL-to-SLF4J bridge for MockWebServer logs on the currently active server. + * Callers must pair every call with {@link #releaseJulBridge()}. + */ + public void acquireJulBridge() { + if (!bridgeAcquired) { + JulSlf4jBridge.install(); + bridgeAcquired = true; + } + } + + /** + * Releases the JUL-to-SLF4J bridge acquired by {@link #acquireJulBridge()}. + */ + public void releaseJulBridge() { + if (bridgeAcquired) { + JulSlf4jBridge.uninstall(); + bridgeAcquired = false; + } + } + public boolean isClosed() { return closed; } diff --git a/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java new file mode 100644 index 00000000..b064d57d --- /dev/null +++ b/apollo-mockserver/src/main/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridge.java @@ -0,0 +1,129 @@ +/* + * Copyright 2026 Apollo Authors + * + * Licensed 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 com.ctrip.framework.apollo.mockserver; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Handler; +import java.util.logging.Logger; +import org.slf4j.LoggerFactory; + +/** + * Routes okhttp's MockWebServer logs (which go through java.util.logging directly) to + * slf4j/logback for the lifetime of active {@link ApolloTestingServer} instances. + * + *

Unlike {@code SLF4JBridgeHandler#install()}, this only touches the JUL logger used by + * MockWebServer, not the JUL root logger, so unrelated JUL handlers elsewhere in the JVM are + * never removed. It also skips installing when the SLF4J binding is slf4j-jdk14, since bridging + * JUL to an SLF4J binding that itself delegates back to JUL creates an infinite loop. Calls are + * reference-counted so concurrently running mock servers don't uninstall the bridge out from + * under each other. + * + *

Uses reflection to instantiate {@code SLF4JBridgeHandler} so the class can load even when + * {@code jul-to-slf4j} is not on the classpath. + */ +final class JulSlf4jBridge { + + private static final org.slf4j.Logger LOG = LoggerFactory.getLogger(JulSlf4jBridge.class); + private static final String MOCK_WEB_SERVER_LOGGER_NAME = "okhttp3.mockwebserver.MockWebServer"; + private static final String SLF4J_BRIDGE_HANDLER_CLASS = "org.slf4j.bridge.SLF4JBridgeHandler"; + private static final AtomicInteger ACTIVE = new AtomicInteger(0); + + private static Handler[] originalHandlers; + private static boolean originalUseParentHandlers; + private static boolean bridged; + private static Logger mockWebServerLogger; + private static Handler bridgeHandler; + + private JulSlf4jBridge() {} + + static synchronized void install() { + if (ACTIVE.getAndIncrement() > 0) { + return; + } + bridged = false; + if (!isBridgeHandlerAvailable()) { + LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because " + + "jul-to-slf4j is not on the classpath"); + return; + } + if (isJdk14Binding(LoggerFactory.getILoggerFactory().getClass().getName())) { + LOG.warn("Skipping JUL-to-SLF4J bridge for MockWebServer logs because the SLF4J binding " + + "is slf4j-jdk14; bridging it would create an infinite JUL<->SLF4J logging loop."); + return; + } + mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + originalHandlers = mockWebServerLogger.getHandlers(); + originalUseParentHandlers = mockWebServerLogger.getUseParentHandlers(); + for (Handler handler : originalHandlers) { + mockWebServerLogger.removeHandler(handler); + } + bridgeHandler = newBridgeHandler(); + mockWebServerLogger.addHandler(bridgeHandler); + mockWebServerLogger.setUseParentHandlers(false); + bridged = true; + } + + private static boolean isBridgeHandlerAvailable() { + try { + Class.forName(SLF4J_BRIDGE_HANDLER_CLASS); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } + + private static Handler newBridgeHandler() { + try { + return (Handler) Class.forName(SLF4J_BRIDGE_HANDLER_CLASS).newInstance(); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new IllegalStateException( + "SLF4JBridgeHandler was available during check but not during instantiation", e); + } + } + + static synchronized void uninstall() { + if (ACTIVE.decrementAndGet() > 0 || !bridged) { + return; + } + mockWebServerLogger.removeHandler(bridgeHandler); + for (Handler handler : originalHandlers) { + if (!containsHandler(mockWebServerLogger.getHandlers(), handler)) { + mockWebServerLogger.addHandler(handler); + } + } + mockWebServerLogger.setUseParentHandlers(originalUseParentHandlers); + originalHandlers = null; + mockWebServerLogger = null; + bridgeHandler = null; + bridged = false; + } + + private static boolean containsHandler(Handler[] handlers, Handler target) { + for (Handler handler : handlers) { + if (handler == target) { + return true; + } + } + return false; + } + + /** Package-visible for testing without depending on the actual runtime SLF4J binding. */ + static boolean isJdk14Binding(String iLoggerFactoryClassName) { + return "org.slf4j.impl.JDK14LoggerFactory".equals(iLoggerFactoryClassName) + || "org.slf4j.jul.JDK14LoggerFactory".equals(iLoggerFactoryClassName); + } +} diff --git a/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java new file mode 100644 index 00000000..482cb8f7 --- /dev/null +++ b/apollo-mockserver/src/test/java/com/ctrip/framework/apollo/mockserver/JulSlf4jBridgeTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2026 Apollo Authors + * + * Licensed 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 com.ctrip.framework.apollo.mockserver; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.lang.reflect.Field; +import java.util.logging.Handler; +import java.util.logging.Logger; +import org.junit.jupiter.api.Test; +import org.slf4j.bridge.SLF4JBridgeHandler; + +class JulSlf4jBridgeTest { + + private static final String MOCK_WEB_SERVER_LOGGER_NAME = "okhttp3.mockwebserver.MockWebServer"; + + @Test + void isJdk14BindingDetectsKnownFactoryClassNames() { + assertTrue(JulSlf4jBridge.isJdk14Binding("org.slf4j.impl.JDK14LoggerFactory")); + assertTrue(JulSlf4jBridge.isJdk14Binding("org.slf4j.jul.JDK14LoggerFactory")); + assertFalse(JulSlf4jBridge.isJdk14Binding("ch.qos.logback.classic.LoggerContext")); + } + + @Test + void installOnlyTouchesMockWebServerLoggerNotRoot() { + Logger root = Logger.getLogger(""); + Handler[] rootHandlersBefore = root.getHandlers(); + + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + Handler[] mwsHandlersBefore = mockWebServerLogger.getHandlers(); + boolean useParentBefore = mockWebServerLogger.getUseParentHandlers(); + + JulSlf4jBridge.install(); + try { + assertArrayEquals(rootHandlersBefore, root.getHandlers(), + "installing the bridge must not touch the JUL root logger's handlers"); + assertTrue(hasBridgeHandler(mockWebServerLogger), + "MockWebServer's own logger should get the bridge handler"); + assertFalse(mockWebServerLogger.getUseParentHandlers()); + } finally { + JulSlf4jBridge.uninstall(); + } + + assertArrayEquals(rootHandlersBefore, root.getHandlers()); + assertArrayEquals(mwsHandlersBefore, mockWebServerLogger.getHandlers()); + assertEquals(useParentBefore, mockWebServerLogger.getUseParentHandlers()); + } + + @Test + void closingSameServerTwiceDoesNotDisruptAnotherActiveServer() throws Exception { + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + ApolloTestingServer serverA = new ApolloTestingServer(); + ApolloTestingServer serverB = new ApolloTestingServer(); + try { + serverA.start(); + serverA.acquireJulBridge(); + serverB.start(); + serverB.acquireJulBridge(); + assertTrue(hasBridgeHandler(mockWebServerLogger)); + + serverA.releaseJulBridge(); + serverA.releaseJulBridge(); + + assertTrue(hasBridgeHandler(mockWebServerLogger), + "bridge must remain installed while server B is still running"); + } finally { + serverA.releaseJulBridge(); + serverA.close(); + serverB.releaseJulBridge(); + serverB.close(); + } + + assertFalse(hasBridgeHandler(mockWebServerLogger)); + } + + @Test + void closingNeverStartedServerDoesNotDisruptActiveServer() throws Exception { + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + ApolloTestingServer active = new ApolloTestingServer(); + ApolloTestingServer neverStarted = new ApolloTestingServer(); + try { + active.start(); + active.acquireJulBridge(); + assertTrue(hasBridgeHandler(mockWebServerLogger)); + + neverStarted.releaseJulBridge(); + + assertTrue(hasBridgeHandler(mockWebServerLogger), + "releasing a bridge that was never acquired must not affect the active bridge"); + } finally { + active.releaseJulBridge(); + active.close(); + neverStarted.close(); + } + + assertFalse(hasBridgeHandler(mockWebServerLogger)); + } + + @Test + void bridgeRetainsLoggerUntilMockWebServerInitializes() throws Exception { + ApolloTestingServer server = new ApolloTestingServer() { + @Override + public void resetOverriddenProperties() { + System.gc(); + super.resetOverriddenProperties(); + } + }; + try { + server.start(); + server.acquireJulBridge(); + assertTrue(hasBridgeHandler(Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME)), + "the MockWebServer logger must retain its bridge across GC during startup"); + } finally { + server.releaseJulBridge(); + server.close(); + } + } + + @Test + void startupFailureDoesNotInstallBridge() throws Exception { + Logger mockWebServerLogger = Logger.getLogger(MOCK_WEB_SERVER_LOGGER_NAME); + ApolloTestingServer server = new ApolloTestingServer(); + + Field locatorField = ApolloTestingServer.class.getDeclaredField("CONFIG_SERVICE_LOCATOR"); + locatorField.setAccessible(true); + Object originalLocator = locatorField.get(null); + locatorField.set(null, null); + try { + assertThrows(IllegalStateException.class, server::start, "start() must surface failures"); + assertFalse(hasBridgeHandler(mockWebServerLogger), + "a failed start() must not install the JUL bridge"); + } finally { + locatorField.set(null, originalLocator); + server.close(); + } + } + + private static boolean hasBridgeHandler(Logger logger) { + for (Handler handler : logger.getHandlers()) { + if (handler instanceof SLF4JBridgeHandler) { + return true; + } + } + return false; + } +}