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.
*/
public static void clearCache() {
JarFileUrlKey.clearCache();
JarUrlConnection.clearCache();
}

View File

@ -16,68 +16,56 @@
package org.springframework.boot.loader.net.protocol.jar;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Objects;
/**
* Utility to generate a string key from a jar file {@link URL} that can be used as a
* cache key.
* A fast cache key for a jar file {@link URL} that doesn't trigger DNS lookups.
*
* @author Phillip Webb
*/
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());
}
/**
* Get the {@link JarFileUrlKey} for the given URL.
* @param url the source URL
* @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);
@Override
public int hashCode() {
return Objects.hashCode(this.file);
}
private static boolean isCachableUrl(URL url) {
// Don't cache URL that have a host since equals() will perform DNS lookup
return url.getHost() == null || url.getHost().isEmpty();
@Override
public boolean equals(Object obj) {
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) {
StringBuilder value = new StringBuilder();
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;
private boolean equalsIgnoringCase(String s1, String s2) {
return (s1 == s2) || (s1 != null && s1.equalsIgnoreCase(s2));
}
}

View File

@ -144,7 +144,7 @@ class UrlJarFiles {
*/
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<>();
@ -154,7 +154,7 @@ class UrlJarFiles {
* @return the cached {@link JarFile} or {@code null}
*/
JarFile get(URL jarFileUrl) {
String urlKey = JarFileUrlKey.get(jarFileUrl);
JarFileUrlKey urlKey = new JarFileUrlKey(jarFileUrl);
synchronized (this) {
return this.jarFileUrlToJarFile.get(urlKey);
}
@ -180,7 +180,7 @@ class UrlJarFiles {
* they were already there
*/
boolean putIfAbsent(URL jarFileUrl, JarFile jarFile) {
String urlKey = JarFileUrlKey.get(jarFileUrl);
JarFileUrlKey urlKey = new JarFileUrlKey(jarFileUrl);
synchronized (this) {
JarFile cached = this.jarFileUrlToJarFile.get(urlKey);
if (cached == null) {
@ -200,7 +200,7 @@ class UrlJarFiles {
synchronized (this) {
URL removedUrl = this.jarFileToJarFileUrl.remove(jarFile);
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;
import java.net.MalformedURLException;
import java.net.URL;
import org.junit.jupiter.api.BeforeAll;
@ -38,51 +39,66 @@ class JarFileUrlKeyTests {
}
@Test
void getCreatesKey() throws Exception {
URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path");
void equalsAndHashCode() throws Exception {
JarFileUrlKey k1 = key("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
void getWhenUppercaseProtocolCreatesKey() throws Exception {
URL url = new URL("JAR:nested:/my.jar/!mynested.jar!/my/path");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path");
void equalsWhenUppercaseAndLowercaseProtocol() throws Exception {
JarFileUrlKey k1 = key("JAR:nested:/my.jar/!mynested.jar!/my/path");
JarFileUrlKey k2 = key("jar:nested:/my.jar/!mynested.jar!/my/path");
assertThat(k1).isEqualTo(k2);
}
@Test
void getWhenHasHostAndPortCreatesKey() throws Exception {
URL url = new URL("https://example.com:1234/test");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test");
void equalsWhenHasHostAndPort() throws Exception {
JarFileUrlKey k1 = key("https://example.com:1234/test");
JarFileUrlKey k2 = key("https://example.com:1234/test");
assertThat(k1).isEqualTo(k2);
}
@Test
void getWhenHasUppercaseHostCreatesKey() throws Exception {
URL url = new URL("https://EXAMPLE.com:1234/test");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:1234/test");
void equalsWhenHasUppercaseAndLowercaseHost() throws Exception {
JarFileUrlKey k1 = key("https://EXAMPLE.com:1234/test");
JarFileUrlKey k2 = key("https://example.com:1234/test");
assertThat(k1).isEqualTo(k2);
}
@Test
void getWhenHasNoPortCreatesKeyWithDefaultPort() throws Exception {
URL url = new URL("https://EXAMPLE.com/test");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443/test");
void equalsWhenHasNoPortUsesDefaultPort() throws Exception {
JarFileUrlKey k1 = key("https://EXAMPLE.com/test");
JarFileUrlKey k2 = key("https://example.com:443/test");
assertThat(k1).isEqualTo(k2);
}
@Test
void getWhenHasNoFileCreatesKey() throws Exception {
URL url = new URL("https://EXAMPLE.com");
assertThat(JarFileUrlKey.get(url)).isEqualTo("https:example.com:443");
void equalsWhenHasNoFile() throws Exception {
JarFileUrlKey k1 = key("https://EXAMPLE.com");
JarFileUrlKey k2 = key("https://example.com:443");
assertThat(k1).isEqualTo(k2);
}
@Test
void getWhenHasRuntimeRefCreatesKey() throws Exception {
URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#runtime");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path#runtime");
void equalsWhenHasRuntimeRef() throws Exception {
JarFileUrlKey k1 = key("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
void getWhenHasOtherRefCreatesKeyWithoutRef() throws Exception {
URL url = new URL("jar:nested:/my.jar/!mynested.jar!/my/path#example");
assertThat(JarFileUrlKey.get(url)).isEqualTo("jar:nested:/my.jar/!mynested.jar!/my/path");
void equalsWhenHasOtherRefIgnoresRefs() throws Exception {
JarFileUrlKey k1 = key("jar:nested:/my.jar/!mynested.jar!/my/path#example");
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));
}
}