Skip to content

Commit ea5f128

Browse files
committed
fix: handle nested jar class path fallback
Fixes apolloconfig/apollo#5592
1 parent c89320a commit ea5f128

File tree

4 files changed

+189
-18
lines changed

4 files changed

+189
-18
lines changed

CHANGES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Apollo Java 2.6.0
66

77
------------------
88

9-
*
9+
* [Fix Apollo client local cache fallback for Spring Boot 3 executable JARs](https://github.com/apolloconfig/apollo-java/pull/136)
1010

1111
------------------
1212
All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/6?closed=1)

apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@
3232
import com.ctrip.framework.apollo.spi.ConfigFactoryManager;
3333
import com.ctrip.framework.apollo.spi.ConfigRegistry;
3434
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
35+
import com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer;
3536
import com.google.common.collect.Table;
3637
import java.lang.reflect.Field;
3738
import java.lang.reflect.Method;
39+
import java.util.HashMap;
3840
import java.util.Map;
3941
import java.util.concurrent.ArrayBlockingQueue;
4042
import java.util.concurrent.BlockingQueue;
@@ -80,13 +82,14 @@ public class ConfigDataIntegrationTest {
8082
private static final EmbeddedApollo embeddedApollo = new EmbeddedApollo();
8183

8284
private static final ExternalResource apolloStateResource = new ExternalResource() {
83-
private String originalAppId;
8485
private String originalEnv;
86+
private Map<String, String> originalApolloSystemProperties = new HashMap<>();
8587

8688
@Override
8789
protected void before() throws Throwable {
88-
originalAppId = System.getProperty("app.id");
90+
originalApolloSystemProperties = snapshotApolloSystemProperties();
8991
originalEnv = System.getProperty("env");
92+
clearApolloSystemProperties();
9093
System.setProperty("app.id", TEST_APP_ID);
9194
System.setProperty("env", TEST_ENV);
9295
resetApolloStaticState();
@@ -99,7 +102,7 @@ protected void after() {
99102
} catch (Exception ex) {
100103
throw new RuntimeException(ex);
101104
} finally {
102-
restoreOrClear("app.id", originalAppId);
105+
restoreApolloSystemProperties(originalApolloSystemProperties);
103106
restoreOrClear("env", originalEnv);
104107
}
105108
}
@@ -118,6 +121,9 @@ public void beforeEach() {
118121
@After
119122
public void afterEach() throws Exception {
120123
resetApolloStaticState();
124+
clearApolloSystemProperties();
125+
System.setProperty("app.id", TEST_APP_ID);
126+
System.setProperty("env", TEST_ENV);
121127
}
122128

123129
@Autowired
@@ -283,6 +289,27 @@ private static void restoreOrClear(String key, String originalValue) {
283289
System.setProperty(key, originalValue);
284290
}
285291

292+
private static Map<String, String> snapshotApolloSystemProperties() {
293+
Map<String, String> originalProperties = new HashMap<>();
294+
for (String propertyName : ApolloApplicationContextInitializer.APOLLO_SYSTEM_PROPERTIES) {
295+
originalProperties.put(propertyName, System.getProperty(propertyName));
296+
}
297+
return originalProperties;
298+
}
299+
300+
private static void clearApolloSystemProperties() {
301+
for (String propertyName : ApolloApplicationContextInitializer.APOLLO_SYSTEM_PROPERTIES) {
302+
System.clearProperty(propertyName);
303+
}
304+
}
305+
306+
private static void restoreApolloSystemProperties(Map<String, String> originalProperties) {
307+
clearApolloSystemProperties();
308+
for (Map.Entry<String, String> entry : originalProperties.entrySet()) {
309+
restoreOrClear(entry.getKey(), entry.getValue());
310+
}
311+
}
312+
286313
private static void addOrModifyForAllAppIds(String namespace, String key, String value) {
287314
embeddedApollo.addOrModifyProperty(TEST_APP_ID, namespace, key, value);
288315
embeddedApollo.addOrModifyProperty(

apollo-core/src/main/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtil.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@
1717
package com.ctrip.framework.apollo.core.utils;
1818

1919
import com.google.common.base.Strings;
20-
2120
import org.slf4j.Logger;
2221
import org.slf4j.LoggerFactory;
23-
2422
import java.net.URL;
2523
import java.net.URLDecoder;
2624

@@ -40,19 +38,12 @@ public class ClassLoaderUtil {
4038
}
4139

4240
try {
43-
URL url = loader.getResource("");
44-
// get class path
45-
if (url != null) {
46-
classPath = url.getPath();
47-
classPath = URLDecoder.decode(classPath, "utf-8");
48-
}
49-
50-
// 如果是jar包内的,则返回当前路径
51-
if (Strings.isNullOrEmpty(classPath) || classPath.contains(".jar!")) {
52-
classPath = System.getProperty("user.dir");
41+
classPath = resolveClassPath(loader, null);
42+
if (Strings.isNullOrEmpty(classPath)) {
43+
classPath = getDefaultClassPath();
5344
}
5445
} catch (Throwable ex) {
55-
classPath = System.getProperty("user.dir");
46+
classPath = getDefaultClassPath();
5647
logger.warn("Failed to locate class path, fallback to user.dir: {}", classPath, ex);
5748
}
5849
}
@@ -65,6 +56,23 @@ public static String getClassPath() {
6556
return classPath;
6657
}
6758

59+
static String resolveClassPath(ClassLoader classLoader, String defaultClassPath) throws Exception {
60+
URL url = classLoader.getResource("");
61+
if (url == null || !"file".equalsIgnoreCase(url.getProtocol())) {
62+
return defaultClassPath;
63+
}
64+
65+
String resolvedClassPath = URLDecoder.decode(url.getPath(), "utf-8");
66+
if (Strings.isNullOrEmpty(resolvedClassPath)) {
67+
return defaultClassPath;
68+
}
69+
return resolvedClassPath;
70+
}
71+
72+
private static String getDefaultClassPath() {
73+
return System.getProperty("user.dir");
74+
}
75+
6876
public static boolean isClassPresent(String className) {
6977
try {
7078
Class.forName(className);

apollo-core/src/test/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtilTest.java

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,79 @@
1818

1919
import static org.junit.Assert.*;
2020

21+
import java.io.ByteArrayOutputStream;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.lang.reflect.Method;
25+
import java.net.URL;
26+
import java.net.URLDecoder;
27+
import java.net.URLConnection;
28+
import java.net.URLStreamHandler;
29+
import java.nio.file.Files;
30+
import java.nio.file.Path;
2131
import org.junit.Test;
2232

2333
public class ClassLoaderUtilTest {
2434
private static boolean shouldFailInInitialization = false;
35+
2536
@Test
2637
public void testGetClassLoader() {
2738
assertNotNull(ClassLoaderUtil.getLoader());
2839
}
2940

41+
@Test
42+
public void testResolveClassPathWithFileUrl() throws Exception {
43+
Path tempDir = Files.createTempDirectory("apollo class path");
44+
try {
45+
ClassLoader classLoader = classLoaderReturning(tempDir.toUri().toURL());
46+
47+
assertEquals(URLDecoder.decode(tempDir.toUri().toURL().getPath(), "utf-8"),
48+
ClassLoaderUtil.resolveClassPath(classLoader, "fallback"));
49+
} finally {
50+
Files.deleteIfExists(tempDir);
51+
}
52+
}
53+
54+
@Test
55+
public void testResolveClassPathFallsBackForNestedJarUrl() throws Exception {
56+
String fallback = "/tmp/fallback";
57+
URL nestedJarUrl = createUrl("jar:nested:/tmp/apollo-app.jar/!BOOT-INF/classes/!/");
58+
ClassLoader classLoader = classLoaderReturning(nestedJarUrl);
59+
60+
assertEquals(fallback, ClassLoaderUtil.resolveClassPath(classLoader, fallback));
61+
}
62+
63+
@Test
64+
public void testResolveClassPathPreservesWindowsStyleFileUrlFormat() throws Exception {
65+
URL windowsFileUrl = new URL("file:/C:/Program%20Files/apollo/classes/");
66+
ClassLoader classLoader = classLoaderReturning(windowsFileUrl);
67+
68+
assertEquals("/C:/Program Files/apollo/classes/",
69+
ClassLoaderUtil.resolveClassPath(classLoader, "fallback"));
70+
}
71+
72+
@Test
73+
public void testGetClassPathFallsBackToUserDirForNestedJarUrl() throws Exception {
74+
String expectedClassPath = System.getProperty("user.dir");
75+
ClassLoader contextClassLoader =
76+
classLoaderReturning(createUrl("jar:nested:/tmp/apollo-app.jar/!BOOT-INF/classes/!/"));
77+
78+
assertEquals(expectedClassPath, isolatedClassPath(contextClassLoader));
79+
}
80+
81+
@Test
82+
public void testGetClassPathFallsBackToUserDirWhenLookupFails() throws Exception {
83+
String expectedClassPath = System.getProperty("user.dir");
84+
ClassLoader contextClassLoader = new ClassLoader(null) {
85+
@Override
86+
public URL getResource(String name) {
87+
throw new RuntimeException("lookup failed");
88+
}
89+
};
90+
91+
assertEquals(expectedClassPath, isolatedClassPath(contextClassLoader));
92+
}
93+
3094
@Test
3195
public void testIsClassPresent() {
3296
assertTrue(ClassLoaderUtil.isClassPresent("java.lang.String"));
@@ -50,4 +114,76 @@ public static class ClassWithInitializationError {
50114
}
51115
}
52116
}
53-
}
117+
118+
private ClassLoader classLoaderReturning(URL resource) {
119+
return new ClassLoader(null) {
120+
@Override
121+
public URL getResource(String name) {
122+
return resource;
123+
}
124+
};
125+
}
126+
127+
private URL createUrl(String spec) throws Exception {
128+
return new URL(null, spec, new URLStreamHandler() {
129+
@Override
130+
protected URLConnection openConnection(URL url) {
131+
throw new UnsupportedOperationException();
132+
}
133+
});
134+
}
135+
136+
private String isolatedClassPath(ClassLoader contextClassLoader) throws Exception {
137+
ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
138+
try {
139+
Thread.currentThread().setContextClassLoader(contextClassLoader);
140+
Class<?> isolatedClassLoaderUtil = newIsolatedClassLoader().loadClass(
141+
ClassLoaderUtil.class.getName());
142+
Method getClassPath = isolatedClassLoaderUtil.getMethod("getClassPath");
143+
return (String) getClassPath.invoke(null);
144+
} finally {
145+
Thread.currentThread().setContextClassLoader(originalClassLoader);
146+
}
147+
}
148+
149+
private ClassLoader newIsolatedClassLoader() throws IOException {
150+
String className = ClassLoaderUtil.class.getName();
151+
String classFile = className.replace('.', '/') + ".class";
152+
byte[] classBytes = readClassBytes(classFile);
153+
154+
return new ClassLoader(ClassLoaderUtil.class.getClassLoader()) {
155+
@Override
156+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
157+
if (!className.equals(name)) {
158+
return super.loadClass(name, resolve);
159+
}
160+
161+
synchronized (getClassLoadingLock(name)) {
162+
Class<?> loadedClass = findLoadedClass(name);
163+
if (loadedClass == null) {
164+
loadedClass = defineClass(name, classBytes, 0, classBytes.length);
165+
}
166+
if (resolve) {
167+
resolveClass(loadedClass);
168+
}
169+
return loadedClass;
170+
}
171+
}
172+
};
173+
}
174+
175+
private byte[] readClassBytes(String classFile) throws IOException {
176+
try (InputStream inputStream = ClassLoaderUtil.class.getClassLoader().getResourceAsStream(
177+
classFile);
178+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
179+
assertNotNull(inputStream);
180+
181+
byte[] buffer = new byte[1024];
182+
int bytesRead;
183+
while ((bytesRead = inputStream.read(buffer)) != -1) {
184+
outputStream.write(buffer, 0, bytesRead);
185+
}
186+
return outputStream.toByteArray();
187+
}
188+
}
189+
}

0 commit comments

Comments
 (0)