From 82791b4edafc165b7ceffa1b9e8dc0299790c5de Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 17 Dec 2020 21:47:26 -0800 Subject: [PATCH] Improve performance of Tomcat 'jar:war:file' URLs Update jar `Handler` fallback logic to directly support Tomcat 'jar:war:file' URLs. This commit allows contents to be accessed without the JDK needing to extracted the nested jar to the temporary folder. Closes gh-24553 --- .../boot/loader/jar/Handler.java | 43 ++++++++++++++++++- .../boot/loaderapp/LoaderTestApplication.java | 9 ++++ .../boot/loader/LoaderIntegrationTests.java | 3 +- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java index 1b4cbe0bbb58..fad95b607e44 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jar/Handler.java @@ -47,6 +47,8 @@ public class Handler extends URLStreamHandler { private static final String FILE_PROTOCOL = "file:"; + private static final String TOMCAT_WARFILE_PROTOCOL = "war:file:"; + private static final String SEPARATOR = "!/"; private static final Pattern SEPARATOR_PATTERN = Pattern.compile(SEPARATOR, Pattern.LITERAL); @@ -102,7 +104,8 @@ private boolean isUrlInJarFile(URL url, JarFile jarFile) throws MalformedURLExce private URLConnection openFallbackConnection(URL url, Exception reason) throws IOException { try { - URLConnection connection = openFallbackContextConnection(url); + URLConnection connection = openFallbackTomcatConnection(url); + connection = (connection != null) ? connection : openFallbackContextConnection(url); return (connection != null) ? connection : openFallbackHandlerConnection(url); } catch (Exception ex) { @@ -118,6 +121,44 @@ private URLConnection openFallbackConnection(URL url, Exception reason) throws I } } + /** + * Attempt to open a Tomcat formatted 'jar:war:file:...' URL. This method allows us to + * use our own nested JAR support to open the content rather than the logic in + * {@code sun.net.www.protocol.jar.URLJarFile} which will extract the nested jar to + * the temp folder to that its content can be accessed. + * @param url the URL to open + * @return a {@link URLConnection} or {@code null} + */ + private URLConnection openFallbackTomcatConnection(URL url) { + String file = url.getFile(); + if (isTomcatWarUrl(file)) { + file = file.substring(TOMCAT_WARFILE_PROTOCOL.length()); + file = file.replaceFirst("\\*/", "!/"); + try { + URLConnection connection = openConnection(new URL("jar:file:" + file)); + connection.getInputStream().close(); + return connection; + } + catch (IOException ex) { + } + } + return null; + } + + private boolean isTomcatWarUrl(String file) { + if (file.startsWith(TOMCAT_WARFILE_PROTOCOL) || !file.contains("*/")) { + try { + URLConnection connection = new URL(file).openConnection(); + if (connection.getClass().getName().startsWith("org.apache.catalina")) { + return true; + } + } + catch (Exception ex) { + } + } + return false; + } + /** * Attempt to open a fallback connection by using a context URL captured before the * jar handler was replaced with our own version. Since this method doesn't use diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java index 8619eac57e5d..8829ffda79ad 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/spring-boot-loader-tests-app/src/main/java/org/springframework/boot/loaderapp/LoaderTestApplication.java @@ -16,6 +16,8 @@ package org.springframework.boot.loaderapp; +import java.io.File; +import java.net.JarURLConnection; import java.net.URL; import java.util.Arrays; @@ -33,7 +35,14 @@ public class LoaderTestApplication { @Bean public CommandLineRunner commandLineRunner(ServletContext servletContext) { return (args) -> { + File temp = new File(System.getProperty("java.io.tmpdir")); URL resourceUrl = servletContext.getResource("webjars/jquery/3.5.0/jquery.js"); + JarURLConnection connection = (JarURLConnection) resourceUrl.openConnection(); + String jarName = connection.getJarFile().getName(); + System.out.println(">>>>> jar file " + jarName); + if(jarName.contains(temp.getAbsolutePath())) { + System.out.println(">>>>> jar written to temp"); + } byte[] resourceContent = FileCopyUtils.copyToByteArray(resourceUrl.openStream()); URL directUrl = new URL(resourceUrl.toExternalForm()); byte[] directContent = FileCopyUtils.copyToByteArray(directUrl.openStream()); diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java index ca1a8a3434d2..1abe0c519cf1 100644 --- a/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-loader-tests/src/intTest/java/org/springframework/boot/loader/LoaderIntegrationTests.java @@ -59,8 +59,9 @@ private static File findApplication() { @Test void readUrlsWithoutWarning() { + System.out.println(output.toUtf8String()); assertThat(output.toUtf8String()).contains(">>>>> 287649 BYTES from").doesNotContain("WARNING:") - .doesNotContain("illegal"); + .doesNotContain("illegal").doesNotContain("jar written to temp"); } }