From fd9b2b114edcc1f18e2967c47868a7ffcafa5798 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 28 Sep 2023 23:13:49 -0700 Subject: [PATCH] Improve Tomcat performance when using nested jars Add `NestedJarResourceSet` which can be used for nested jar URLs and unlike the standard Tomcat implementation does not assume that the JAR is backed by a single file. Closes gh-37452 --- .../embedded/tomcat/NestedJarResourceSet.java | 161 ++++++++++++++++++ .../tomcat/TomcatServletWebServerFactory.java | 40 ++++- 2 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java new file mode 100644 index 0000000000..0f1be9560a --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/NestedJarResourceSet.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.web.embedded.tomcat; + +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; +import org.apache.catalina.WebResourceSet; +import org.apache.catalina.webresources.AbstractSingleArchiveResourceSet; +import org.apache.catalina.webresources.JarResource; + +import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; + +/** + * A {@link WebResourceSet} for a resource in a nested JAR. + * + * @author Phillip Webb + */ +class NestedJarResourceSet extends AbstractSingleArchiveResourceSet { + + private static final Name MULTI_RELEASE = new Name("Multi-Release"); + + private final URL url; + + private JarFile archive = null; + + private long archiveUseCount = 0; + + private boolean useCaches; + + private volatile Boolean multiRelease; + + NestedJarResourceSet(URL url, WebResourceRoot root, String webAppMount, String internalPath) + throws IllegalArgumentException { + this.url = url; + setRoot(root); + setWebAppMount(webAppMount); + setInternalPath(internalPath); + setStaticOnly(true); + if (getRoot().getState().isAvailable()) { + try { + start(); + } + catch (LifecycleException ex) { + throw new IllegalStateException(ex); + } + } + } + + @Override + protected WebResource createArchiveResource(JarEntry jarEntry, String webAppPath, Manifest manifest) { + return new JarResource(this, webAppPath, getBaseUrlString(), jarEntry); + } + + @Override + protected void initInternal() throws LifecycleException { + try { + JarURLConnection connection = connect(); + try { + setManifest(connection.getManifest()); + setBaseUrl(connection.getJarFileURL()); + } + finally { + if (!connection.getUseCaches()) { + connection.getJarFile().close(); + } + } + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + protected JarFile openJarFile() throws IOException { + synchronized (this.archiveLock) { + if (this.archive == null) { + JarURLConnection connection = connect(); + this.useCaches = connection.getUseCaches(); + this.archive = connection.getJarFile(); + } + this.archiveUseCount++; + return this.archive; + } + } + + @Override + protected void closeJarFile() { + synchronized (this.archiveLock) { + this.archiveUseCount--; + } + } + + @Override + protected boolean isMultiRelease() { + if (this.multiRelease == null) { + synchronized (this.archiveLock) { + if (this.multiRelease == null) { + // JarFile.isMultiRelease() is final so we must go to the manifest + Manifest manifest = getManifest(); + Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null; + this.multiRelease = (attributes != null) ? attributes.containsKey(MULTI_RELEASE) : false; + } + } + } + return this.multiRelease.booleanValue(); + } + + @Override + public void gc() { + synchronized (this.archiveLock) { + if (this.archive != null && this.archiveUseCount == 0) { + try { + if (!this.useCaches) { + this.archive.close(); + } + } + catch (IOException ex) { + // Ignore + } + this.archive = null; + this.archiveEntries = null; + } + } + } + + private JarURLConnection connect() throws IOException { + URLConnection connection = this.url.openConnection(); + ResourceUtils.useCachesIfNecessary(connection); + Assert.state(connection instanceof JarURLConnection, + () -> "URL '%s' did not return a JAR connection".formatted(this.url)); + connection.connect(); + return (JarURLConnection) connection; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index ca5e6fa1d4..7f05d87a11 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -19,6 +19,7 @@ package org.springframework.boot.web.embedded.tomcat; import java.io.File; import java.io.InputStream; import java.lang.reflect.Method; +import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -46,6 +47,7 @@ import org.apache.catalina.LifecycleListener; import org.apache.catalina.Manager; import org.apache.catalina.Valve; import org.apache.catalina.WebResource; +import org.apache.catalina.WebResourceRoot; import org.apache.catalina.WebResourceRoot.ResourceSetType; import org.apache.catalina.WebResourceSet; import org.apache.catalina.Wrapper; @@ -772,6 +774,10 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto private final class StaticResourceConfigurer implements LifecycleListener { + private static final String WEB_APP_MOUNT = "/"; + + private static final String INTERNAL_PATH = "/META-INF/resources"; + private final Context context; private StaticResourceConfigurer(Context context) { @@ -804,23 +810,39 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto private void addResourceSet(String resource) { try { - if (isInsideNestedJar(resource)) { - // It's a nested jar but we now don't want the suffix because Tomcat - // is going to try and locate it as a root URL (not the resource - // inside it) - resource = resource.substring(0, resource.length() - 2); + if (isInsideClassicNestedJar(resource)) { + addClassicNestedResourceSet(resource); + return; } + WebResourceRoot root = this.context.getResources(); URL url = new URL(resource); - String path = "/META-INF/resources"; - this.context.getResources().createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/", url, path); + if (isInsideNestedJar(resource)) { + root.addJarResources(new NestedJarResourceSet(url, root, WEB_APP_MOUNT, INTERNAL_PATH)); + } + else { + root.createWebResourceSet(ResourceSetType.RESOURCE_JAR, WEB_APP_MOUNT, url, INTERNAL_PATH); + } } catch (Exception ex) { // Ignore (probably not a directory) } } - private boolean isInsideNestedJar(String dir) { - return dir.indexOf("!/") < dir.lastIndexOf("!/"); + private void addClassicNestedResourceSet(String resource) throws MalformedURLException { + // It's a nested jar but we now don't want the suffix because Tomcat + // is going to try and locate it as a root URL (not the resource + // inside it) + URL url = new URL(resource.substring(0, resource.length() - 2)); + this.context.getResources() + .createWebResourceSet(ResourceSetType.RESOURCE_JAR, WEB_APP_MOUNT, url, INTERNAL_PATH); + } + + private boolean isInsideClassicNestedJar(String resource) { + return !isInsideNestedJar(resource) && resource.indexOf("!/") < resource.lastIndexOf("!/"); + } + + private boolean isInsideNestedJar(String resource) { + return resource.startsWith("jar:nested:"); } }