From b1d66ee1b03f8ac8d61ae3d4ef0e027e37e3b60d 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 4aac6aa40d1ac79081296ed5ef3afbcd983cbe86 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 8c1f08e5bced83d9af2470023e65747eaf07fe4b 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 b276b6716c8fb369ab2c4399c5c4664d2493b49c 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 df00f4418e9e31f9633c5707bc4c3701f805062b 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 5464b7033ed159f23e74fadcba81d77178ab4e8f 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 fe502bca596cf54911838b1328341fff74f5b3cd 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 681abd4dd6d6e0b252152d0aa462f52dddb549d9 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 e9a50a4fee7d8a3daa5c24f69e71dd49f6d9e382 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 20bd5ec1e0ec5f91d1c0995c3e905542d13f2aa4 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 b151a31249e9e3dbc1600f747a0e248b5d7bef5f 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 053be47a54d894aecf9483f2f57f34c4ce9adcf4 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 0d758f9ca562b86fea42a90e17caced5ad15ed18 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; + } +}