Merge branch '3.5.x'

Closes gh-46403
This commit is contained in:
Phillip Webb 2025-07-10 19:19:21 -07:00
commit 194fe4b644
4 changed files with 79 additions and 76 deletions

View File

@ -183,7 +183,6 @@ public class Handler extends URLStreamHandler {
* Clear any internal caches. * Clear any internal caches.
*/ */
public static void clearCache() { public static void clearCache() {
JarFileUrlKey.clearCache();
JarUrlConnection.clearCache(); JarUrlConnection.clearCache();
} }

View File

@ -16,68 +16,56 @@
package org.springframework.boot.loader.net.protocol.jar; package org.springframework.boot.loader.net.protocol.jar;
import java.lang.ref.SoftReference;
import java.net.URL; import java.net.URL;
import java.util.Locale; import java.util.Objects;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* Utility to generate a string key from a jar file {@link URL} that can be used as a * A fast cache key for a jar file {@link URL} that doesn't trigger DNS lookups.
* cache key.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
final class JarFileUrlKey { final class JarFileUrlKey {
private static volatile SoftReference<Map<URL, String>> cache; private final String protocol;
private JarFileUrlKey() { private final String host;
private final int port;
private final String file;
private final boolean runtimeRef;
JarFileUrlKey(URL url) {
this.protocol = url.getProtocol();
this.host = url.getHost();
this.port = (url.getPort() != -1) ? url.getPort() : url.getDefaultPort();
this.file = url.getFile();
this.runtimeRef = "runtime".equals(url.getRef());
} }
/** @Override
* Get the {@link JarFileUrlKey} for the given URL. public int hashCode() {
* @param url the source URL return Objects.hashCode(this.file);
* @return a {@link JarFileUrlKey} instance
*/
static String get(URL url) {
if (!isCachableUrl(url)) {
return create(url);
}
Map<URL, String> cache = (JarFileUrlKey.cache != null) ? JarFileUrlKey.cache.get() : null;
if (cache == null) {
cache = new ConcurrentHashMap<>();
JarFileUrlKey.cache = new SoftReference<>(cache);
}
return cache.computeIfAbsent(url, JarFileUrlKey::create);
} }
private static boolean isCachableUrl(URL url) { @Override
// Don't cache URL that have a host since equals() will perform DNS lookup public boolean equals(Object obj) {
return url.getHost() == null || url.getHost().isEmpty(); if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
JarFileUrlKey other = (JarFileUrlKey) obj;
// We check file first as case sensitive and the most likely item to be different
return Objects.equals(this.file, other.file) && equalsIgnoringCase(this.protocol, other.protocol)
&& equalsIgnoringCase(this.host, other.host) && (this.port == other.port)
&& (this.runtimeRef == other.runtimeRef);
} }
private static String create(URL url) { private boolean equalsIgnoringCase(String s1, String s2) {
StringBuilder value = new StringBuilder(); return (s1 == s2) || (s1 != null && s1.equalsIgnoreCase(s2));
String protocol = url.getProtocol();
String host = url.getHost();
int port = (url.getPort() != -1) ? url.getPort() : url.getDefaultPort();
String file = url.getFile();
value.append(protocol.toLowerCase(Locale.ROOT));
value.append(":");
if (host != null && !host.isEmpty()) {
value.append(host.toLowerCase(Locale.ROOT));
value.append((port != -1) ? ":" + port : "");
}
value.append((file != null) ? file : "");
if ("runtime".equals(url.getRef())) {
value.append("#runtime");
}
return value.toString();
}
static void clearCache() {
cache = null;
} }
} }

View File

@ -144,7 +144,7 @@ class UrlJarFiles {
*/ */
private static final class Cache { private static final class Cache {
private final Map<String, JarFile> jarFileUrlToJarFile = new HashMap<>(); private final Map<JarFileUrlKey, JarFile> jarFileUrlToJarFile = new HashMap<>();
private final Map<JarFile, URL> jarFileToJarFileUrl = new HashMap<>(); private final Map<JarFile, URL> jarFileToJarFileUrl = new HashMap<>();
@ -154,7 +154,7 @@ class UrlJarFiles {
* @return the cached {@link JarFile} or {@code null} * @return the cached {@link JarFile} or {@code null}
*/ */
JarFile get(URL jarFileUrl) { JarFile get(URL jarFileUrl) {
String urlKey = JarFileUrlKey.get(jarFileUrl); JarFileUrlKey urlKey = new JarFileUrlKey(jarFileUrl);
synchronized (this) { synchronized (this) {
return this.jarFileUrlToJarFile.get(urlKey); return this.jarFileUrlToJarFile.get(urlKey);
} }
@ -180,7 +180,7 @@ class UrlJarFiles {
* they were already there * they were already there
*/ */
boolean putIfAbsent(URL jarFileUrl, JarFile jarFile) { boolean putIfAbsent(URL jarFileUrl, JarFile jarFile) {
String urlKey = JarFileUrlKey.get(jarFileUrl); JarFileUrlKey urlKey = new JarFileUrlKey(jarFileUrl);
synchronized (this) { synchronized (this) {
JarFile cached = this.jarFileUrlToJarFile.get(urlKey); JarFile cached = this.jarFileUrlToJarFile.get(urlKey);
if (cached == null) { if (cached == null) {
@ -200,7 +200,7 @@ class UrlJarFiles {
synchronized (this) { synchronized (this) {
URL removedUrl = this.jarFileToJarFileUrl.remove(jarFile); URL removedUrl = this.jarFileToJarFileUrl.remove(jarFile);
if (removedUrl != null) { if (removedUrl != null) {
this.jarFileUrlToJarFile.remove(JarFileUrlKey.get(removedUrl)); this.jarFileUrlToJarFile.remove(new JarFileUrlKey(removedUrl));
} }
} }
} }

View File

@ -16,6 +16,7 @@
package org.springframework.boot.loader.net.protocol.jar; package org.springframework.boot.loader.net.protocol.jar;
import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
@ -38,51 +39,66 @@ class JarFileUrlKeyTests {
} }
@Test @Test
void getCreatesKey() throws Exception { void equalsAndHashCode() throws Exception {
URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path"); JarFileUrlKey k1 = key("jar:nested:/my.jar/!mynested.jar!/my/path");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path"); JarFileUrlKey k2 = key("jar:nested:/my.jar/!mynested.jar!/my/path");
JarFileUrlKey k3 = key("jar:nested:/my.jar/!mynested.jar!/my/path2");
assertThat(k1.hashCode()).isEqualTo(k2.hashCode())
.isEqualTo("nested:/my.jar/!mynested.jar!/my/path".hashCode());
assertThat(k1).isEqualTo(k1).isEqualTo(k2).isNotEqualTo(k3);
} }
@Test @Test
void getWhenUppercaseProtocolCreatesKey() throws Exception { void equalsWhenUppercaseAndLowercaseProtocol() throws Exception {
URL url = new URL("JAR:nested:/my.jar/!mynested.jar!/my/path"); JarFileUrlKey k1 = key("JAR:nested:/my.jar/!mynested.jar!/my/path");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path"); JarFileUrlKey k2 = key("jar:nested:/my.jar/!mynested.jar!/my/path");
assertThat(k1).isEqualTo(k2);
} }
@Test @Test
void getWhenHasHostAndPortCreatesKey() throws Exception { void equalsWhenHasHostAndPort() throws Exception {
URL url = new URL("https://example.com:1234/test"); JarFileUrlKey k1 = key("https://example.com:1234/test");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test"); JarFileUrlKey k2 = key("https://example.com:1234/test");
assertThat(k1).isEqualTo(k2);
} }
@Test @Test
void getWhenHasUppercaseHostCreatesKey() throws Exception { void equalsWhenHasUppercaseAndLowercaseHost() throws Exception {
URL url = new URL("https://EXAMPLE.com:1234/test"); JarFileUrlKey k1 = key("https://EXAMPLE.com:1234/test");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test"); JarFileUrlKey k2 = key("https://example.com:1234/test");
assertThat(k1).isEqualTo(k2);
} }
@Test @Test
void getWhenHasNoPortCreatesKeyWithDefaultPort() throws Exception { void equalsWhenHasNoPortUsesDefaultPort() throws Exception {
URL url = new URL("https://EXAMPLE.com/test"); JarFileUrlKey k1 = key("https://EXAMPLE.com/test");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443/test"); JarFileUrlKey k2 = key("https://example.com:443/test");
assertThat(k1).isEqualTo(k2);
} }
@Test @Test
void getWhenHasNoFileCreatesKey() throws Exception { void equalsWhenHasNoFile() throws Exception {
URL url = new URL("https://EXAMPLE.com"); JarFileUrlKey k1 = key("https://EXAMPLE.com");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443"); JarFileUrlKey k2 = key("https://example.com:443");
assertThat(k1).isEqualTo(k2);
} }
@Test @Test
void getWhenHasRuntimeRefCreatesKey() throws Exception { void equalsWhenHasRuntimeRef() throws Exception {
URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#runtime"); JarFileUrlKey k1 = key("jar:nested:/my.jar/!mynested.jar!/my/path#runtime");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path#runtime"); JarFileUrlKey k2 = key("jar:nested:/my.jar/!mynested.jar!/my/path#runtime");
assertThat(k1).isEqualTo(k2);
} }
@Test @Test
void getWhenHasOtherRefCreatesKeyWithoutRef() throws Exception { void equalsWhenHasOtherRefIgnoresRefs() throws Exception {
URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#example"); JarFileUrlKey k1 = key("jar:nested:/my.jar/!mynested.jar!/my/path#example");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path"); JarFileUrlKey k2 = key("jar:nested:/my.jar/!mynested.jar!/my/path");
assertThat(k1).isEqualTo(k2);
}
private JarFileUrlKey key(String spec) throws MalformedURLException {
return new JarFileUrlKey(new URL(spec));
} }
} }