Fix static resource handling when run in IDE or using Maven or Gradle

The changes made for gh-8299 attempted to make static resource
handling consistent across Jetty, Tomcat, and Undertow. They did so
for application's launched using JarLauncher or WarLauncher but did
not consider application's launched in an IDE or using spring-boot:run
in Maven or bootRun in Gradle.

Running in an IDE or via Maven or Gradle introduces two new
resource locations:

 - Jars on the classpath with file protocol URLs (they are always
   jar protocol URLs when using either launcher)
 - Directories on the classpath from a project that is depended upon
   and contains resources in META-INF/resources

This commit updates the factories for all three containers to handle
these new resources locations. The integration tests have also been
updated.
This commit is contained in:
Andy Wilkinson 2017-03-09 12:22:55 +00:00
parent 67068fc83d
commit a2cf0455fd
14 changed files with 604 additions and 45 deletions

View File

@ -34,8 +34,6 @@ import org.springframework.util.FileCopyUtils;
*/
abstract class AbstractApplicationLauncher extends ExternalResource {
private final File serverPortFile = new File("target/server.port");
private final ApplicationBuilder applicationBuilder;
private Process process;
@ -62,8 +60,15 @@ abstract class AbstractApplicationLauncher extends ExternalResource {
protected abstract List<String> getArguments(File archive);
protected abstract File getWorkingDirectory();
protected abstract String getDescription(String packaging);
private Process startApplication() throws Exception {
this.serverPortFile.delete();
File workingDirectory = getWorkingDirectory();
File serverPortFile = workingDirectory == null ? new File("target/server.port")
: new File(workingDirectory, "target/server.port");
serverPortFile.delete();
File archive = this.applicationBuilder.buildApplication();
List<String> arguments = new ArrayList<String>();
arguments.add(System.getProperty("java.home") + "/bin/java");
@ -72,14 +77,17 @@ abstract class AbstractApplicationLauncher extends ExternalResource {
arguments.toArray(new String[arguments.size()]));
processBuilder.redirectOutput(Redirect.INHERIT);
processBuilder.redirectError(Redirect.INHERIT);
if (workingDirectory != null) {
processBuilder.directory(workingDirectory);
}
Process process = processBuilder.start();
this.httpPort = awaitServerPort(process);
this.httpPort = awaitServerPort(process, serverPortFile);
return process;
}
private int awaitServerPort(Process process) throws Exception {
private int awaitServerPort(Process process, File serverPortFile) throws Exception {
long end = System.currentTimeMillis() + 30000;
while (this.serverPortFile.length() == 0) {
while (serverPortFile.length() == 0) {
if (System.currentTimeMillis() > end) {
throw new IllegalStateException(
"server.port file was not written within 30 seconds");
@ -89,8 +97,8 @@ abstract class AbstractApplicationLauncher extends ExternalResource {
}
Thread.sleep(100);
}
return Integer.parseInt(
FileCopyUtils.copyToString(new FileReader(this.serverPortFile)));
return Integer
.parseInt(FileCopyUtils.copyToString(new FileReader(serverPortFile)));
}
}

View File

@ -19,6 +19,8 @@ package org.springframework.boot.context.embedded;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -47,29 +49,38 @@ public abstract class AbstractEmbeddedServletContainerIntegrationTests {
protected final RestTemplate rest = new RestTemplate();
public static Object[] parameters(String packaging) {
public static Object[] parameters(String packaging,
List<Class<? extends AbstractApplicationLauncher>> applicationLaunchers) {
List<Object> parameters = new ArrayList<Object>();
parameters.addAll(createParameters(packaging, "jetty", "current"));
parameters.addAll(
createParameters(packaging, "tomcat", "current", "8.0.41", "7.0.75"));
parameters.addAll(createParameters(packaging, "undertow", "current"));
parameters.addAll(createParameters(packaging, "jetty",
Collections.singletonList("current"), applicationLaunchers));
parameters.addAll(createParameters(packaging, "tomcat",
Arrays.asList("current", "8.0.41", "7.0.75"), applicationLaunchers));
parameters.addAll(createParameters(packaging, "undertow",
Collections.singletonList("current"), applicationLaunchers));
return parameters.toArray(new Object[parameters.size()]);
}
private static List<Object> createParameters(String packaging, String container,
String... versions) {
List<String> versions,
List<Class<? extends AbstractApplicationLauncher>> applicationLaunchers) {
List<Object> parameters = new ArrayList<Object>();
for (String version : versions) {
ApplicationBuilder applicationBuilder = new ApplicationBuilder(
temporaryFolder, packaging, container, version);
parameters.add(new Object[] {
StringUtils.capitalise(container) + " " + version + " packaged "
+ packaging,
new PackagedApplicationLauncher(applicationBuilder) });
parameters.add(new Object[] {
StringUtils.capitalise(container) + " " + version + " exploded "
+ packaging,
new ExplodedApplicationLauncher(applicationBuilder) });
for (Class<? extends AbstractApplicationLauncher> launcherClass : applicationLaunchers) {
try {
AbstractApplicationLauncher launcher = launcherClass
.getDeclaredConstructor(ApplicationBuilder.class)
.newInstance(applicationBuilder);
String name = StringUtils.capitalise(container) + " " + version + ": "
+ launcher.getDescription(packaging);
parameters.add(new Object[] { name, launcher });
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
return parameters;
}

View File

@ -0,0 +1,141 @@
/*
* 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.context.embedded;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
* {@link AbstractApplicationLauncher} that launches a Spring Boot application with a
* classpath similar to that used when run with Maven or Gradle.
*
* @author Andy Wilkinson
*/
class BootRunApplicationLauncher extends AbstractApplicationLauncher {
private final File exploded = new File("target/run");
BootRunApplicationLauncher(ApplicationBuilder applicationBuilder) {
super(applicationBuilder);
}
@Override
protected List<String> getArguments(File archive) {
try {
explodeArchive(archive);
deleteLauncherClasses();
File targetClasses = populateTargetClasses(archive);
File dependencies = populateDependencies(archive);
populateSrcMainWebapp();
List<String> classpath = new ArrayList<String>();
classpath.add(targetClasses.getAbsolutePath());
for (File dependency : dependencies.listFiles()) {
classpath.add(dependency.getAbsolutePath());
}
return Arrays.asList("-cp",
StringUtils.collectionToDelimitedString(classpath,
File.pathSeparator),
"com.example.ResourceHandlingApplication");
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private void deleteLauncherClasses() {
FileSystemUtils.deleteRecursively(new File(this.exploded, "org"));
}
private File populateTargetClasses(File archive) {
File targetClasses = new File(this.exploded, "target/classes");
targetClasses.mkdirs();
new File(this.exploded, getClassesPath(archive)).renameTo(targetClasses);
return targetClasses;
}
private File populateDependencies(File archive) {
File dependencies = new File(this.exploded, "dependencies");
dependencies.mkdirs();
List<String> libPaths = getLibPaths(archive);
for (String libPath : libPaths) {
for (File jar : new File(this.exploded, libPath).listFiles()) {
jar.renameTo(new File(dependencies, jar.getName()));
}
}
return dependencies;
}
private void populateSrcMainWebapp() {
File srcMainWebapp = new File(this.exploded, "src/main/webapp");
srcMainWebapp.mkdirs();
new File(this.exploded, "webapp-resource.txt")
.renameTo(new File(srcMainWebapp, "webapp-resource.txt"));
}
private String getClassesPath(File archive) {
return archive.getName().endsWith(".jar") ? "BOOT-INF/classes"
: "WEB-INF/classes";
}
private List<String> getLibPaths(File archive) {
return archive.getName().endsWith(".jar")
? Collections.singletonList("BOOT-INF/lib")
: Arrays.asList("WEB-INF/lib", "WEB-INF/lib-provided");
}
private void explodeArchive(File archive) throws IOException {
FileSystemUtils.deleteRecursively(this.exploded);
JarFile jarFile = new JarFile(archive);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
File extracted = new File(this.exploded, jarEntry.getName());
if (jarEntry.isDirectory()) {
extracted.mkdirs();
}
else {
FileOutputStream extractedOutputStream = new FileOutputStream(extracted);
StreamUtils.copy(jarFile.getInputStream(jarEntry), extractedOutputStream);
extractedOutputStream.close();
}
}
jarFile.close();
}
@Override
protected File getWorkingDirectory() {
return this.exploded;
}
@Override
protected String getDescription(String packaging) {
return "build system run " + packaging + " project";
}
}

View File

@ -0,0 +1,67 @@
/*
* 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.context.embedded;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for Spring Boot's embedded servlet container support when developing
* a jar application.
*
* @author Andy Wilkinson
*/
@RunWith(Parameterized.class)
public class EmbeddedServletContainerJarDevelopmentIntegrationTests
extends AbstractEmbeddedServletContainerIntegrationTests {
@Parameters(name = "{0}")
public static Object[] parameters() {
return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar", Arrays
.asList(BootRunApplicationLauncher.class, IdeApplicationLauncher.class));
}
public EmbeddedServletContainerJarDevelopmentIntegrationTests(String name,
AbstractApplicationLauncher launcher) {
super(name, launcher);
}
@Test
public void metaInfResourceFromDependencyIsAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void metaInfResourceFromDependencyIsAvailableViaServletContext()
throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}

View File

@ -16,6 +16,8 @@
package org.springframework.boot.context.embedded;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@ -38,7 +40,9 @@ public class EmbeddedServletContainerJarPackagingIntegrationTests
@Parameters(name = "{0}")
public static Object[] parameters() {
return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar");
return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar",
Arrays.asList(PackagedApplicationLauncher.class,
ExplodedApplicationLauncher.class));
}
public EmbeddedServletContainerJarPackagingIntegrationTests(String name,

View File

@ -0,0 +1,74 @@
/*
* 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.context.embedded;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for Spring Boot's embedded servlet container support when developing
* a war application.
*
* @author Andy Wilkinson
*/
@RunWith(Parameterized.class)
public class EmbeddedServletContainerWarDevelopmentIntegrationTests
extends AbstractEmbeddedServletContainerIntegrationTests {
@Parameters(name = "{0}")
public static Object[] parameters() {
return AbstractEmbeddedServletContainerIntegrationTests.parameters("war", Arrays
.asList(BootRunApplicationLauncher.class, IdeApplicationLauncher.class));
}
public EmbeddedServletContainerWarDevelopmentIntegrationTests(String name,
AbstractApplicationLauncher launcher) {
super(name, launcher);
}
@Test
public void metaInfResourceFromDependencyIsAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void metaInfResourceFromDependencyIsAvailableViaServletContext()
throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void webappResourcesAreAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest.getForEntity("/webapp-resource.txt",
String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}

View File

@ -16,6 +16,8 @@
package org.springframework.boot.context.embedded;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@ -38,7 +40,9 @@ public class EmbeddedServletContainerWarPackagingIntegrationTests
@Parameters(name = "{0}")
public static Object[] parameters() {
return AbstractEmbeddedServletContainerIntegrationTests.parameters("war");
return AbstractEmbeddedServletContainerIntegrationTests.parameters("war",
Arrays.asList(PackagedApplicationLauncher.class,
ExplodedApplicationLauncher.class));
}
public EmbeddedServletContainerWarPackagingIntegrationTests(String name,

View File

@ -29,8 +29,8 @@ import org.springframework.util.FileSystemUtils;
import org.springframework.util.StreamUtils;
/**
* {@link AbstractApplicationLauncher} that launches an exploded Spring Boot application
* using Spring Boot's Jar or War launcher.
* {@link AbstractApplicationLauncher} that launches a Spring Boot application using
* {@code JarLauncher} or {@code WarLauncher} and an exploded archive.
*
* @author Andy Wilkinson
*/
@ -42,6 +42,16 @@ class ExplodedApplicationLauncher extends AbstractApplicationLauncher {
super(applicationBuilder);
}
@Override
protected File getWorkingDirectory() {
return this.exploded;
}
@Override
protected String getDescription(String packaging) {
return "exploded " + packaging;
}
@Override
protected List<String> getArguments(File archive) {
String mainClass = archive.getName().endsWith(".war")

View File

@ -0,0 +1,152 @@
/*
* 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.context.embedded;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
* {@link AbstractApplicationLauncher} that launches a Spring Boot application with a
* classpath similar to that used when run in an IDE.
*
* @author Andy Wilkinson
*/
class IdeApplicationLauncher extends AbstractApplicationLauncher {
private final File exploded = new File("target/ide");
IdeApplicationLauncher(ApplicationBuilder applicationBuilder) {
super(applicationBuilder);
}
@Override
protected File getWorkingDirectory() {
return this.exploded;
}
@Override
protected String getDescription(String packaging) {
return "IDE run " + packaging + " project";
}
@Override
protected List<String> getArguments(File archive) {
try {
explodeArchive(archive, this.exploded);
deleteLauncherClasses();
File targetClasses = populateTargetClasses(archive);
File dependencies = populateDependencies(archive);
File resourcesProject = explodedResourcesProject(dependencies);
populateSrcMainWebapp();
List<String> classpath = new ArrayList<String>();
classpath.add(targetClasses.getAbsolutePath());
for (File dependency : dependencies.listFiles()) {
classpath.add(dependency.getAbsolutePath());
}
classpath.add(resourcesProject.getAbsolutePath());
return Arrays.asList("-cp",
StringUtils.collectionToDelimitedString(classpath,
File.pathSeparator),
"com.example.ResourceHandlingApplication");
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private File populateTargetClasses(File archive) {
File targetClasses = new File(this.exploded, "target/classes");
targetClasses.mkdirs();
new File(this.exploded, getClassesPath(archive)).renameTo(targetClasses);
return targetClasses;
}
private File populateDependencies(File archive) {
File dependencies = new File(this.exploded, "dependencies");
dependencies.mkdirs();
List<String> libPaths = getLibPaths(archive);
for (String libPath : libPaths) {
for (File jar : new File(this.exploded, libPath).listFiles()) {
jar.renameTo(new File(dependencies, jar.getName()));
}
}
return dependencies;
}
private File explodedResourcesProject(File dependencies) throws IOException {
File resourcesProject = new File(this.exploded,
"resources-project/target/classes");
File resourcesJar = new File(dependencies, "resources-1.0.jar");
explodeArchive(resourcesJar, resourcesProject);
resourcesJar.delete();
return resourcesProject;
}
private void populateSrcMainWebapp() {
File srcMainWebapp = new File(this.exploded, "src/main/webapp");
srcMainWebapp.mkdirs();
new File(this.exploded, "webapp-resource.txt")
.renameTo(new File(srcMainWebapp, "webapp-resource.txt"));
}
private void deleteLauncherClasses() {
FileSystemUtils.deleteRecursively(new File(this.exploded, "org"));
}
private String getClassesPath(File archive) {
return archive.getName().endsWith(".jar") ? "BOOT-INF/classes"
: "WEB-INF/classes";
}
private List<String> getLibPaths(File archive) {
return archive.getName().endsWith(".jar")
? Collections.singletonList("BOOT-INF/lib")
: Arrays.asList("WEB-INF/lib", "WEB-INF/lib-provided");
}
private void explodeArchive(File archive, File destination) throws IOException {
FileSystemUtils.deleteRecursively(destination);
JarFile jarFile = new JarFile(archive);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
File extracted = new File(destination, jarEntry.getName());
if (jarEntry.isDirectory()) {
extracted.mkdirs();
}
else {
FileOutputStream extractedOutputStream = new FileOutputStream(extracted);
StreamUtils.copy(jarFile.getInputStream(jarEntry), extractedOutputStream);
extractedOutputStream.close();
}
}
jarFile.close();
}
}

View File

@ -32,6 +32,16 @@ class PackagedApplicationLauncher extends AbstractApplicationLauncher {
super(applicationBuilder);
}
@Override
protected File getWorkingDirectory() {
return null;
}
@Override
protected String getDescription(String packaging) {
return "packaged " + packaging;
}
@Override
protected List<String> getArguments(File archive) {
return Arrays.asList("-jar", archive.getAbsolutePath());

View File

@ -96,15 +96,23 @@ public abstract class AbstractEmbeddedServletContainerFactory
if (classLoader instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
try {
URLConnection connection = url.openConnection();
if (connection instanceof JarURLConnection) {
JarURLConnection jarConnection = (JarURLConnection) connection;
JarFile jar = jarConnection.getJarFile();
if (jar.getName().endsWith(".jar")
&& jar.getJarEntry("META-INF/resources") != null) {
if ("file".equals(url.getProtocol())) {
File file = new File(url.getFile());
if (file.isDirectory()
&& new File(file, "META-INF/resources").isDirectory()) {
staticResourceUrls.add(url);
}
jar.close();
else if (isResourcesJar(file)) {
staticResourceUrls.add(url);
}
}
else {
URLConnection connection = url.openConnection();
if (connection instanceof JarURLConnection) {
if (isResourcesJar((JarURLConnection) connection)) {
staticResourceUrls.add(url);
}
}
}
}
catch (IOException ex) {
@ -115,6 +123,34 @@ public abstract class AbstractEmbeddedServletContainerFactory
return staticResourceUrls;
}
private boolean isResourcesJar(JarURLConnection connection) {
try {
return isResourcesJar(connection.getJarFile());
}
catch (IOException ex) {
return false;
}
}
private boolean isResourcesJar(File file) {
try {
return isResourcesJar(new JarFile(file));
}
catch (IOException ex) {
return false;
}
}
private boolean isResourcesJar(JarFile jar) throws IOException {
try {
return jar.getName().endsWith(".jar")
&& (jar.getJarEntry("META-INF/resources") != null);
}
finally {
jar.close();
}
}
File getExplodedWarFileDocumentRoot(File codeSourceFile) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Code archive: " + codeSourceFile);

View File

@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded.jetty;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
@ -410,8 +411,7 @@ public class JettyEmbeddedServletContainerFactory
root.isDirectory() ? Resource.newResource(root.getCanonicalFile())
: JarResource.newJarResource(Resource.newResource(root)));
for (URL resourceJarUrl : this.getUrlsOfJarsWithMetaInfResources()) {
Resource resource = Resource
.newResource(resourceJarUrl + "META-INF/resources");
Resource resource = createResource(resourceJarUrl);
// Jetty 9.2 and earlier do not support nested jars. See
// https://github.com/eclipse/jetty.project/issues/518
if (resource.exists() && resource.isDirectory()) {
@ -426,6 +426,16 @@ public class JettyEmbeddedServletContainerFactory
}
}
private Resource createResource(URL url) throws MalformedURLException {
if ("file".equals(url.getProtocol())) {
File file = new File(url.getFile());
if (file.isFile()) {
return Resource.newResource("jar:" + url + "!/META-INF/resources");
}
}
return Resource.newResource(url + "META-INF/resources");
}
/**
* Add Jetty's {@code DefaultServlet} to the given {@link WebAppContext}.
* @param context the jetty {@link WebAppContext}

View File

@ -22,7 +22,6 @@ import java.net.URL;
import java.util.List;
import javax.naming.directory.DirContext;
import javax.servlet.ServletContext;
import org.apache.catalina.Context;
import org.apache.catalina.WebResourceRoot.ResourceSetType;
@ -57,6 +56,9 @@ abstract class TomcatResources {
}
addJar(jar);
}
else {
addDir(file, url);
}
}
}
@ -127,7 +129,7 @@ abstract class TomcatResources {
@Override
protected void addDir(String dir, URL url) {
if (getContext() instanceof ServletContext) {
if (getContext() instanceof StandardContext) {
try {
Class<?> fileDirContextClass = Class
.forName("org.apache.naming.resources.FileDirContext");

View File

@ -18,6 +18,7 @@ package org.springframework.boot.context.embedded.undertow;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
@ -468,11 +469,35 @@ public class UndertowEmbeddedServletContainerFactory
private ResourceManager getDocumentRootResourceManager() {
File root = getCanonicalDocumentRoot();
List<URL> metaInfResourceJarUrls = getUrlsOfJarsWithMetaInfResources();
List<URL> metaInfResourceUrls = getUrlsOfJarsWithMetaInfResources();
List<URL> resourceJarUrls = new ArrayList<URL>();
List<ResourceManager> resourceManagers = new ArrayList<ResourceManager>();
ResourceManager rootResourceManager = root.isDirectory()
? new FileResourceManager(root, 0) : new JarResourceManager(root);
return new CompositeResourceManager(rootResourceManager,
new MetaInfResourcesResourceManager(metaInfResourceJarUrls));
resourceManagers.add(rootResourceManager);
for (URL url : metaInfResourceUrls) {
if ("file".equals(url.getProtocol())) {
File file = new File(url.getFile());
if (file.isFile()) {
try {
resourceJarUrls.add(new URL("jar:" + url + "!/"));
}
catch (MalformedURLException ex) {
throw new RuntimeException(ex);
}
}
else {
resourceManagers.add(new FileResourceManager(
new File(file, "META-INF/resources"), 0));
}
}
else {
resourceJarUrls.add(url);
}
}
resourceManagers.add(new MetaInfResourcesResourceManager(resourceJarUrls));
return new CompositeResourceManager(
resourceManagers.toArray(new ResourceManager[resourceManagers.size()]));
}
/**
@ -617,12 +642,17 @@ public class UndertowEmbeddedServletContainerFactory
}
@Override
public Resource getResource(String path) throws IOException {
public Resource getResource(String path) {
for (URL url : this.metaInfResourceJarUrls) {
URL resourceUrl = new URL(url + "META-INF/resources" + path);
URLConnection connection = resourceUrl.openConnection();
if (connection.getContentLength() >= 0) {
return new URLResource(resourceUrl, connection, path);
try {
URL resourceUrl = new URL(url + "META-INF/resources" + path);
URLConnection connection = resourceUrl.openConnection();
if (connection.getContentLength() >= 0) {
return new URLResource(resourceUrl, connection, path);
}
}
catch (IOException ex) {
// Continue
}
}
return null;