From b1d66ee1b03f8ac8d61ae3d4ef0e027e37e3b60d Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Fri, 18 Sep 2026 15:36:01 +0800 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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) {