From ded7cea7616e792a17f31db563af12b7a6cd78d6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 21 Jul 2017 20:53:32 +0100 Subject: [PATCH] Prevent webapp class loader from finding resources Previously, TomcatEmbeddedWebappClassLoader would find resources in WEB-INF/classes. However, unlike standalone Tomcat, we know that in a Boot app WEB-INF/classes is on the class path of the parent class loader so the resources will be found when the parent is queried (which happens as part of the normal search algortithm for both getResource(String) and getResources(String)). This commit overrides findResource(String) and findResources(String) to return null and an empty enumeration respectively. This prevents TomcatEmbeddedWebappClassLoader from finding resources in WEB-INF/classes and returning war: URLs for them that duplicate the jar: URLs that will be found when the parent is queried. Closes gh-9014 --- .../TomcatEmbeddedWebappClassLoader.java | 13 ++ .../TomcatEmbeddedWebappClassLoaderTests.java | 119 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatEmbeddedWebappClassLoaderTests.java diff --git a/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatEmbeddedWebappClassLoader.java b/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatEmbeddedWebappClassLoader.java index 34e9e58f9e..4a2e92dee1 100644 --- a/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatEmbeddedWebappClassLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatEmbeddedWebappClassLoader.java @@ -16,7 +16,10 @@ package org.springframework.boot.web.embedded.tomcat; +import java.io.IOException; import java.net.URL; +import java.util.Collections; +import java.util.Enumeration; import org.apache.catalina.loader.WebappClassLoader; import org.apache.commons.logging.Log; @@ -44,6 +47,16 @@ public class TomcatEmbeddedWebappClassLoader extends WebappClassLoader { super(parent); } + @Override + public URL findResource(String name) { + return null; + } + + @Override + public Enumeration findResources(String name) throws IOException { + return Collections.emptyEnumeration(); + } + @Override public synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { diff --git a/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatEmbeddedWebappClassLoaderTests.java b/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatEmbeddedWebappClassLoaderTests.java new file mode 100644 index 0000000000..d73840b91d --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/TomcatEmbeddedWebappClassLoaderTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.JarOutputStream; +import java.util.zip.ZipEntry; + +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.loader.WebappClassLoader; +import org.apache.catalina.webresources.StandardRoot; +import org.apache.catalina.webresources.WarResourceSet; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import org.springframework.util.CollectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TomcatEmbeddedWebappClassLoader}. + * + * @author Andy Wilkinson + */ +public class TomcatEmbeddedWebappClassLoaderTests { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void getResourceFindsResourceFromParentClassLoader() throws Exception { + File war = createWar(); + withWebappClassLoader(war, (classLoader) -> { + assertThat(classLoader.getResource("test.txt")) + .isEqualTo(new URL(webInfClassesUrlString(war) + "test.txt")); + }); + } + + @Test + public void getResourcesOnlyFindsResourcesFromParentClassLoader() throws Exception { + File warFile = createWar(); + withWebappClassLoader(warFile, (classLoader) -> { + List urls = new ArrayList<>(); + CollectionUtils.toIterator(classLoader.getResources("test.txt")) + .forEachRemaining(urls::add); + assertThat(urls).containsExactly( + new URL(webInfClassesUrlString(warFile) + "test.txt")); + }); + } + + private void withWebappClassLoader(File war, ClassLoaderConsumer consumer) + throws Exception { + URLClassLoader parent = new URLClassLoader( + new URL[] { new URL(webInfClassesUrlString(war)) }, null); + try (WebappClassLoader classLoader = new TomcatEmbeddedWebappClassLoader( + parent)) { + StandardContext context = new StandardContext(); + context.setName("test"); + StandardRoot resources = new StandardRoot(); + resources.setContext(context); + resources.addJarResources( + new WarResourceSet(resources, "/", war.getAbsolutePath())); + resources.start(); + classLoader.setResources(resources); + classLoader.start(); + consumer.accept(classLoader); + } + } + + private String webInfClassesUrlString(File war) { + return "jar:file:" + war.getAbsolutePath() + "!/WEB-INF/classes/"; + } + + private File createWar() throws IOException, FileNotFoundException { + File warFile = this.temp.newFile("test.war"); + try (JarOutputStream warOut = new JarOutputStream( + new FileOutputStream(warFile))) { + createEntries(warOut, "WEB-INF/", "WEB-INF/classes/", + "WEB-INF/classes/test.txt"); + } + return warFile; + } + + private void createEntries(JarOutputStream out, String... names) throws IOException { + for (String name : names) { + out.putNextEntry(new ZipEntry(name)); + out.closeEntry(); + } + } + + private interface ClassLoaderConsumer { + + void accept(ClassLoader classLoader) throws Exception; + + } + +}