diff --git a/CHANGES.md b/CHANGES.md index 737a4b00..a3a9554e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) diff --git a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java index 69bbc8cb..26c33b83 100644 --- a/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java +++ b/apollo-client-config-data/src/test/java/com/ctrip/framework/apollo/config/data/integration/ConfigDataIntegrationTest.java @@ -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; @@ -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 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(); @@ -99,7 +102,7 @@ protected void after() { } catch (Exception ex) { throw new RuntimeException(ex); } finally { - restoreOrClear("app.id", originalAppId); + restoreApolloSystemProperties(originalApolloSystemProperties); restoreOrClear("env", originalEnv); } } @@ -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 @@ -283,6 +289,27 @@ private static void restoreOrClear(String key, String originalValue) { System.setProperty(key, originalValue); } + private static Map snapshotApolloSystemProperties() { + Map 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 originalProperties) { + clearApolloSystemProperties(); + for (Map.Entry 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( diff --git a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtil.java b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtil.java index 2528e482..5791b4dd 100644 --- a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtil.java +++ b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtil.java @@ -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; @@ -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); } } @@ -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; + } + return resolvedClassPath; + } + + private static String getDefaultClassPath() { + return System.getProperty("user.dir"); + } + public static boolean isClassPresent(String className) { try { Class.forName(className); diff --git a/apollo-core/src/test/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtilTest.java b/apollo-core/src/test/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtilTest.java index 8635d5c4..6e7ebef3 100644 --- a/apollo-core/src/test/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtilTest.java +++ b/apollo-core/src/test/java/com/ctrip/framework/apollo/core/utils/ClassLoaderUtilTest.java @@ -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")); @@ -50,4 +114,76 @@ public static class ClassWithInitializationError { } } } -} \ No newline at end of file + + 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(); + } + } +}