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
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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)

------------------
All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/6?closed=1)
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
import com.ctrip.framework.apollo.spi.ConfigFactoryManager;
import com.ctrip.framework.apollo.spi.ConfigRegistry;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer;
import com.google.common.collect.Table;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
Expand Down Expand Up @@ -80,13 +82,14 @@ public class ConfigDataIntegrationTest {
private static final EmbeddedApollo embeddedApollo = new EmbeddedApollo();

private static final ExternalResource apolloStateResource = new ExternalResource() {
private String originalAppId;
private String originalEnv;
private Map<String, String> originalApolloSystemProperties = new HashMap<>();

@Override
protected void before() throws Throwable {
originalAppId = System.getProperty("app.id");
originalApolloSystemProperties = snapshotApolloSystemProperties();
originalEnv = System.getProperty("env");
clearApolloSystemProperties();
System.setProperty("app.id", TEST_APP_ID);
System.setProperty("env", TEST_ENV);
resetApolloStaticState();
Expand All @@ -99,7 +102,7 @@ protected void after() {
} catch (Exception ex) {
throw new RuntimeException(ex);
} finally {
restoreOrClear("app.id", originalAppId);
restoreApolloSystemProperties(originalApolloSystemProperties);
restoreOrClear("env", originalEnv);
}
}
Expand All @@ -118,6 +121,9 @@ public void beforeEach() {
@After
public void afterEach() throws Exception {
resetApolloStaticState();
clearApolloSystemProperties();
System.setProperty("app.id", TEST_APP_ID);
System.setProperty("env", TEST_ENV);
}

@Autowired
Expand Down Expand Up @@ -283,6 +289,27 @@ private static void restoreOrClear(String key, String originalValue) {
System.setProperty(key, originalValue);
}

private static Map<String, String> snapshotApolloSystemProperties() {
Map<String, String> originalProperties = new HashMap<>();
for (String propertyName : ApolloApplicationContextInitializer.APOLLO_SYSTEM_PROPERTIES) {
originalProperties.put(propertyName, System.getProperty(propertyName));
}
return originalProperties;
}

private static void clearApolloSystemProperties() {
for (String propertyName : ApolloApplicationContextInitializer.APOLLO_SYSTEM_PROPERTIES) {
System.clearProperty(propertyName);
}
}

private static void restoreApolloSystemProperties(Map<String, String> originalProperties) {
clearApolloSystemProperties();
for (Map.Entry<String, String> entry : originalProperties.entrySet()) {
restoreOrClear(entry.getKey(), entry.getValue());
}
}

private static void addOrModifyForAllAppIds(String namespace, String key, String value) {
embeddedApollo.addOrModifyProperty(TEST_APP_ID, namespace, key, value);
embeddedApollo.addOrModifyProperty(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@
package com.ctrip.framework.apollo.core.utils;

import com.google.common.base.Strings;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URL;
import java.net.URLDecoder;

Expand All @@ -40,19 +38,12 @@ public class ClassLoaderUtil {
}

try {
URL url = loader.getResource("");
// get class path
if (url != null) {
classPath = url.getPath();
classPath = URLDecoder.decode(classPath, "utf-8");
}

// 如果是jar包内的,则返回当前路径
if (Strings.isNullOrEmpty(classPath) || classPath.contains(".jar!")) {
classPath = System.getProperty("user.dir");
classPath = resolveClassPath(loader, null);
if (Strings.isNullOrEmpty(classPath)) {
classPath = getDefaultClassPath();
}
} catch (Throwable ex) {
classPath = System.getProperty("user.dir");
classPath = getDefaultClassPath();
logger.warn("Failed to locate class path, fallback to user.dir: {}", classPath, ex);
}
}
Expand All @@ -65,6 +56,23 @@ public static String getClassPath() {
return classPath;
}

static String resolveClassPath(ClassLoader classLoader, String defaultClassPath) throws Exception {
URL url = classLoader.getResource("");
if (url == null || !"file".equalsIgnoreCase(url.getProtocol())) {
return defaultClassPath;
}

String resolvedClassPath = URLDecoder.decode(url.getPath(), "utf-8");
if (Strings.isNullOrEmpty(resolvedClassPath)) {
return defaultClassPath;
}
Comment on lines +59 to +68
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveClassPath now derives the classpath via Paths.get(url.toURI()).toString(), which changes the returned format on Windows (it will no longer include the leading / that URL#getPath() produced). There is existing code in the repo that compensates for the old behavior (e.g. apollo-client/src/test/java/com/ctrip/framework/apollo/BaseIntegrationTest.java strips the leading /), which will become incorrect with this change and can break Windows builds/tests. Consider either preserving the previous Windows formatting (e.g. keep using the decoded url.getPath() for file: URLs) or updating the affected call sites/tests to stop assuming a leading / from getClassPath().

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Preserved the historical file: URL formatting in ClassLoaderUtil. resolveClassPath now decodes url.getPath() instead of using Paths.get(url.toURI()), so the existing Windows-leading-slash behavior is kept for callers that still rely on it. I also added a regression test for a file:/C:/Program%20Files/... URL while keeping the nested-jar fallback unchanged.

return resolvedClassPath;
}

private static String getDefaultClassPath() {
return System.getProperty("user.dir");
}

public static boolean isClassPresent(String className) {
try {
Class.forName(className);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,79 @@

import static org.junit.Assert.*;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.Test;

public class ClassLoaderUtilTest {
private static boolean shouldFailInInitialization = false;

@Test
public void testGetClassLoader() {
assertNotNull(ClassLoaderUtil.getLoader());
}

@Test
public void testResolveClassPathWithFileUrl() throws Exception {
Path tempDir = Files.createTempDirectory("apollo class path");
try {
ClassLoader classLoader = classLoaderReturning(tempDir.toUri().toURL());

assertEquals(URLDecoder.decode(tempDir.toUri().toURL().getPath(), "utf-8"),
ClassLoaderUtil.resolveClassPath(classLoader, "fallback"));
} finally {
Files.deleteIfExists(tempDir);
}
}

@Test
public void testResolveClassPathFallsBackForNestedJarUrl() throws Exception {
String fallback = "/tmp/fallback";
URL nestedJarUrl = createUrl("jar:nested:/tmp/apollo-app.jar/!BOOT-INF/classes/!/");
ClassLoader classLoader = classLoaderReturning(nestedJarUrl);

assertEquals(fallback, ClassLoaderUtil.resolveClassPath(classLoader, fallback));
}

@Test
public void testResolveClassPathPreservesWindowsStyleFileUrlFormat() throws Exception {
URL windowsFileUrl = new URL("file:/C:/Program%20Files/apollo/classes/");
ClassLoader classLoader = classLoaderReturning(windowsFileUrl);

assertEquals("/C:/Program Files/apollo/classes/",
ClassLoaderUtil.resolveClassPath(classLoader, "fallback"));
}

@Test
public void testGetClassPathFallsBackToUserDirForNestedJarUrl() throws Exception {
String expectedClassPath = System.getProperty("user.dir");
ClassLoader contextClassLoader =
classLoaderReturning(createUrl("jar:nested:/tmp/apollo-app.jar/!BOOT-INF/classes/!/"));

assertEquals(expectedClassPath, isolatedClassPath(contextClassLoader));
}

@Test
public void testGetClassPathFallsBackToUserDirWhenLookupFails() throws Exception {
String expectedClassPath = System.getProperty("user.dir");
ClassLoader contextClassLoader = new ClassLoader(null) {
@Override
public URL getResource(String name) {
throw new RuntimeException("lookup failed");
}
};

assertEquals(expectedClassPath, isolatedClassPath(contextClassLoader));
}

@Test
public void testIsClassPresent() {
assertTrue(ClassLoaderUtil.isClassPresent("java.lang.String"));
Expand All @@ -50,4 +114,76 @@ public static class ClassWithInitializationError {
}
}
}
}

private ClassLoader classLoaderReturning(URL resource) {
return new ClassLoader(null) {
@Override
public URL getResource(String name) {
return resource;
}
};
}

private URL createUrl(String spec) throws Exception {
return new URL(null, spec, new URLStreamHandler() {
@Override
protected URLConnection openConnection(URL url) {
throw new UnsupportedOperationException();
}
});
}

private String isolatedClassPath(ClassLoader contextClassLoader) throws Exception {
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(contextClassLoader);
Class<?> isolatedClassLoaderUtil = newIsolatedClassLoader().loadClass(
ClassLoaderUtil.class.getName());
Method getClassPath = isolatedClassLoaderUtil.getMethod("getClassPath");
return (String) getClassPath.invoke(null);
} finally {
Thread.currentThread().setContextClassLoader(originalClassLoader);
}
}

private ClassLoader newIsolatedClassLoader() throws IOException {
String className = ClassLoaderUtil.class.getName();
String classFile = className.replace('.', '/') + ".class";
byte[] classBytes = readClassBytes(classFile);

return new ClassLoader(ClassLoaderUtil.class.getClassLoader()) {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (!className.equals(name)) {
return super.loadClass(name, resolve);
}

synchronized (getClassLoadingLock(name)) {
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass == null) {
loadedClass = defineClass(name, classBytes, 0, classBytes.length);
}
if (resolve) {
resolveClass(loadedClass);
}
return loadedClass;
}
}
};
}

private byte[] readClassBytes(String classFile) throws IOException {
try (InputStream inputStream = ClassLoaderUtil.class.getClassLoader().getResourceAsStream(
classFile);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
assertNotNull(inputStream);

byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
return outputStream.toByteArray();
}
}
}
Loading