Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
6 changes: 6 additions & 0 deletions apollo-mockserver/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.21</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public class ApolloTestingServer implements AutoCloseable {

private boolean closed;

private boolean bridgeAcquired;

static {
try {
System.setProperty("apollo.longPollingInitialDelayInMills", "0");
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading