Make static resource handling consistent across embedded containers

Previously, there were a number of inconsistencies in the embedded
containers' handling of static resources. The Servlet spec requires
that static resources can be served from the META-INF/resources/
directory of jars nested inside a war in WEB-INF/lib/. The intention
was also to extend this to cover jar packaging when jars are nested in
BOOT-INF/lib/. This worked when using Tomcat as long as Jasper was on
the classpath. If you didn't have Jasper on the classpath or you
were using Jetty or Undertow it did not work.

This commit updates the configuration of embedded Jetty, Tomcat, and
Undertow so that all three containers handle static resources in the
same way, serving them from jars in WEB-INF/lib/ or /BOOT-INF/lib/.
Numerous intergration tests have been added to verify the behaviour,
including tests for Tomcat 8.0 and 7.0 which is supported in addition
to the default 8.5.x. Note that static resource handling only works
with Jetty 9.3.x and 9.2 and earlier does not support nested jars (
see https://github.com/eclipse/jetty.project/issues/518 for details).

Closes gh-8299
This commit is contained in:
Andy Wilkinson 2017-02-19 20:28:30 +00:00
parent 21ca1af677
commit b443b745fb
18 changed files with 1112 additions and 42 deletions

View File

@ -22,6 +22,7 @@
</properties>
<modules>
<module>spring-boot-devtools-tests</module>
<module>spring-boot-integration-tests-embedded-servlet-container</module>
<module>spring-boot-gradle-tests</module>
<module>spring-boot-launch-script-tests</module>
<module>spring-boot-security-tests</module>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-integration-tests</artifactId>
<version>1.4.5.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-boot-integration-tests-embedded-servlet-container</artifactId>
<packaging>jar</packaging>
<name>Spring Boot Embedded Servlet Container Integration Tests</name>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.samskivert</groupId>
<artifactId>jmustache</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-invoker</artifactId>
<version>3.0.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,72 @@
/*
* 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 com.example;
import java.io.IOException;
import java.net.URL;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.system.EmbeddedServerPortFileWriter;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
/**
* Test application for verifying an embedded container's static resource handling.
*
* @author Andy Wilkinson
*/
@SpringBootApplication
public class ResourceHandlingApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ResourceHandlingApplication.class)
.properties("server.port:0")
.listeners(new EmbeddedServerPortFileWriter("target/server.port"))
.run(args);
}
@Bean
public ServletRegistrationBean resourceServletRegistration() {
ServletRegistrationBean registration = new ServletRegistrationBean(
new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
URL resource = getServletContext()
.getResource(req.getQueryString());
if (resource == null) {
resp.sendError(404);
}
else {
resp.getWriter().println(resource);
resp.getWriter().flush();
}
}
});
registration.addUrlMappings("/servletContext");
return registration;
}
}

View File

@ -0,0 +1,96 @@
/*
* 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.FileReader;
import java.lang.ProcessBuilder.Redirect;
import java.util.ArrayList;
import java.util.List;
import org.junit.rules.ExternalResource;
import org.springframework.util.FileCopyUtils;
/**
* Base {@link ExternalResource} for launching a Spring Boot application as part of a
* JUnit test.
*
* @author Andy Wilkinson
*/
abstract class AbstractApplicationLauncher extends ExternalResource {
private final File serverPortFile = new File("target/server.port");
private final ApplicationBuilder applicationBuilder;
private Process process;
private int httpPort;
protected AbstractApplicationLauncher(ApplicationBuilder applicationBuilder) {
this.applicationBuilder = applicationBuilder;
}
@Override
protected final void before() throws Throwable {
this.process = startApplication();
}
@Override
protected final void after() {
this.process.destroy();
}
public final int getHttpPort() {
return this.httpPort;
}
protected abstract List<String> getArguments(File archive);
private Process startApplication() throws Exception {
this.serverPortFile.delete();
File archive = this.applicationBuilder.buildApplication();
List<String> arguments = new ArrayList<String>();
arguments.add(System.getProperty("java.home") + "/bin/java");
arguments.addAll(getArguments(archive));
ProcessBuilder processBuilder = new ProcessBuilder(
arguments.toArray(new String[arguments.size()]));
processBuilder.redirectOutput(Redirect.INHERIT);
processBuilder.redirectError(Redirect.INHERIT);
Process process = processBuilder.start();
this.httpPort = awaitServerPort(process);
return process;
}
private int awaitServerPort(Process process) throws Exception {
long end = System.currentTimeMillis() + 30000;
while (this.serverPortFile.length() == 0) {
if (System.currentTimeMillis() > end) {
throw new IllegalStateException(
"server.port file was not written within 30 seconds");
}
if (!process.isAlive()) {
throw new IllegalStateException("Application failed to launch");
}
Thread.sleep(100);
}
return Integer.parseInt(
FileCopyUtils.copyToString(new FileReader(this.serverPortFile)));
}
}

View File

@ -0,0 +1,110 @@
/*
* 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.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.codehaus.plexus.util.StringUtils;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriTemplateHandler;
/**
* Base class for embedded servlet container integration tests.
*
* @author Andy Wilkinson
*/
public abstract class AbstractEmbeddedServletContainerIntegrationTests {
@ClassRule
public static final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public final AbstractApplicationLauncher launcher;
protected final RestTemplate rest = new RestTemplate();
public static Object[] parameters(String packaging) {
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"));
return parameters.toArray(new Object[parameters.size()]);
}
private static List<Object> createParameters(String packaging, String container,
String... versions) {
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) });
}
return parameters;
}
protected AbstractEmbeddedServletContainerIntegrationTests(String name,
AbstractApplicationLauncher launcher) {
this.launcher = launcher;
this.rest.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
});
this.rest.setUriTemplateHandler(new UriTemplateHandler() {
@Override
public URI expand(String uriTemplate, Object... uriVariables) {
return URI.create(
"http://localhost:" + launcher.getHttpPort() + uriTemplate);
}
@Override
public URI expand(String uriTemplate, Map<String, ?> uriVariables) {
return URI.create(
"http://localhost:" + launcher.getHttpPort() + uriTemplate);
}
});
}
}

View File

@ -0,0 +1,159 @@
/*
* Copyright 2012-2016 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.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import com.samskivert.mustache.Mustache;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
import org.apache.maven.shared.invoker.InvocationRequest;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.junit.rules.TemporaryFolder;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Builds a Spring Boot application using Maven. To use this class, the {@code maven.home}
* system property must be set.
*
* @author Andy Wilkinson
*/
class ApplicationBuilder {
private final TemporaryFolder temp;
private final String packaging;
private final String container;
private final String containerVersion;
ApplicationBuilder(TemporaryFolder temp, String packaging, String container,
String containerVersion) {
this.temp = temp;
this.packaging = packaging;
this.container = container;
this.containerVersion = containerVersion;
}
File buildApplication() throws Exception {
File containerFolder = new File(this.temp.getRoot(),
this.container + "-" + this.containerVersion);
if (containerFolder.exists()) {
return new File(containerFolder, "app/target/app-0.0.1." + this.packaging);
}
return doBuildApplication(containerFolder);
}
private File doBuildApplication(File containerFolder)
throws IOException, FileNotFoundException, MavenInvocationException {
File resourcesJar = createResourcesJar();
File appFolder = new File(containerFolder, "app");
appFolder.mkdirs();
writePom(appFolder, resourcesJar);
copyApplicationSource(appFolder);
packageApplication(appFolder);
return new File(appFolder, "target/app-0.0.1." + this.packaging);
}
private File createResourcesJar() throws IOException, FileNotFoundException {
File resourcesJar = new File(this.temp.getRoot(), "resources.jar");
if (resourcesJar.exists()) {
return resourcesJar;
}
JarOutputStream resourcesJarStream = new JarOutputStream(
new FileOutputStream(resourcesJar));
resourcesJarStream.putNextEntry(new ZipEntry("META-INF/resources/"));
resourcesJarStream.closeEntry();
resourcesJarStream.putNextEntry(
new ZipEntry("META-INF/resources/nested-meta-inf-resource.txt"));
resourcesJarStream.write("nested".getBytes());
resourcesJarStream.closeEntry();
resourcesJarStream.close();
return resourcesJar;
}
private void writePom(File appFolder, File resourcesJar)
throws FileNotFoundException, IOException {
Map<String, Object> context = new HashMap<String, Object>();
context.put("packaging", this.packaging);
context.put("container", this.container);
context.put("bootVersion", Versions.getBootVersion());
context.put("resourcesJarPath", resourcesJar.getAbsolutePath());
context.put("containerVersion",
"current".equals(this.containerVersion) ? ""
: String.format("<%s.version>%s</%s.version>", this.container,
this.containerVersion, this.container));
context.put("additionalDependencies", getAdditionalDependencies());
FileWriter out = new FileWriter(new File(appFolder, "pom.xml"));
Mustache.compiler().escapeHTML(false)
.compile(new FileReader("src/test/resources/pom-template.xml"))
.execute(context, out);
out.close();
}
private List<Map<String, String>> getAdditionalDependencies() {
List<Map<String, String>> additionalDependencies = new ArrayList<Map<String, String>>();
if ("tomcat".equals(this.container) && !"current".equals(this.containerVersion)) {
Map<String, String> juli = new HashMap<String, String>();
juli.put("groupId", "org.apache.tomcat");
juli.put("artifactId", "tomcat-juli");
juli.put("version", "${tomcat.version}");
additionalDependencies.add(juli);
}
return additionalDependencies;
}
private void copyApplicationSource(File appFolder) throws IOException {
File examplePackage = new File(appFolder, "src/main/java/com/example");
examplePackage.mkdirs();
FileCopyUtils.copy(
new File("src/test/java/com/example/ResourceHandlingApplication.java"),
new File(examplePackage, "ResourceHandlingApplication.java"));
if ("war".equals(this.packaging)) {
File srcMainWebapp = new File(appFolder, "src/main/webapp");
srcMainWebapp.mkdirs();
FileCopyUtils.copy("webapp resource",
new FileWriter(new File(srcMainWebapp, "webapp-resource.txt")));
}
}
private void packageApplication(File appFolder) throws MavenInvocationException {
InvocationRequest invocation = new DefaultInvocationRequest();
invocation.setBaseDirectory(appFolder);
invocation.setGoals(Collections.singletonList("package"));
InvocationResult execute = new DefaultInvoker().execute(invocation);
assertThat(execute.getExitCode()).isEqualTo(0);
}
}

View File

@ -0,0 +1,85 @@
/*
* 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 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 using jar
* packaging.
*
* @author Andy Wilkinson
*/
@RunWith(Parameterized.class)
public class EmbeddedServletContainerJarPackagingIntegrationTests
extends AbstractEmbeddedServletContainerIntegrationTests {
@Parameters(name = "{0}")
public static Object[] parameters() {
return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar");
}
public EmbeddedServletContainerJarPackagingIntegrationTests(String name,
AbstractApplicationLauncher launcher) {
super(name, launcher);
}
@Test
public void nestedMetaInfResourceIsAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void nestedMetaInfResourceIsAvailableViaServletContext() throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void nestedJarIsNotAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/BOOT-INF/lib/resources-1.0.jar", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void applicationClassesAreNotAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest.getForEntity(
"/BOOT-INF/classes/com/example/ResourceHandlingApplication.class",
String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void launcherIsNotAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest.getForEntity(
"/org/springframework/boot/loader/Launcher.class", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
}

View File

@ -0,0 +1,85 @@
/*
* 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 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 using war
* packaging.
*
* @author Andy Wilkinson
*/
@RunWith(Parameterized.class)
public class EmbeddedServletContainerWarPackagingIntegrationTests
extends AbstractEmbeddedServletContainerIntegrationTests {
@Parameters(name = "{0}")
public static Object[] parameters() {
return AbstractEmbeddedServletContainerIntegrationTests.parameters("war");
}
public EmbeddedServletContainerWarPackagingIntegrationTests(String name,
AbstractApplicationLauncher launcher) {
super(name, launcher);
}
@Test
public void nestedMetaInfResourceIsAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void nestedMetaInfResourceIsAvailableViaServletContext() throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/nested-meta-inf-resource.txt", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
public void nestedJarIsNotAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest
.getForEntity("/WEB-INF/lib/resources-1.0.jar", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@Test
public void applicationClassesAreNotAvailableViaHttp() throws Exception {
ResponseEntity<String> entity = this.rest.getForEntity(
"/WEB-INF/classes/com/example/ResourceHandlingApplication.class",
String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@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

@ -0,0 +1,78 @@
/*
* 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.Arrays;
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;
/**
* {@link AbstractApplicationLauncher} that launches an exploded Spring Boot application
* using Spring Boot's Jar or War launcher.
*
* @author Andy Wilkinson
*/
class ExplodedApplicationLauncher extends AbstractApplicationLauncher {
private final File exploded = new File("target/exploded");
ExplodedApplicationLauncher(ApplicationBuilder applicationBuilder) {
super(applicationBuilder);
}
@Override
protected List<String> getArguments(File archive) {
String mainClass = archive.getName().endsWith(".war")
? "org.springframework.boot.loader.WarLauncher"
: "org.springframework.boot.loader.JarLauncher";
try {
explodeArchive(archive);
return Arrays.asList("-cp", this.exploded.getAbsolutePath(), mainClass);
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
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();
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.util.Arrays;
import java.util.List;
/**
* {@link AbstractApplicationLauncher} that launches a packaged Spring Boot application
* using {@code java -jar}.
*
* @author Andy Wilkinson
*/
class PackagedApplicationLauncher extends AbstractApplicationLauncher {
PackagedApplicationLauncher(ApplicationBuilder applicationBuilder) {
super(applicationBuilder);
}
@Override
protected List<String> getArguments(File archive) {
return Arrays.asList("-jar", archive.getAbsolutePath());
}
}

View File

@ -0,0 +1,56 @@
/*
* 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.FileReader;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import org.xml.sax.InputSource;
/**
* Provides access to dependency versions by querying the project's pom.
*
* @author Andy Wilkinson
*/
final class Versions {
private Versions() {
}
public static String getBootVersion() {
return evaluateExpression(
"/*[local-name()='project']/*[local-name()='parent']/*[local-name()='version']"
+ "/text()");
}
private static String evaluateExpression(String expression) {
try {
XPathFactory xPathFactory = XPathFactory.newInstance();
XPath xpath = xPathFactory.newXPath();
XPathExpression expr = xpath.compile(expression);
String version = expr.evaluate(new InputSource(new FileReader("pom.xml")));
return version;
}
catch (Exception ex) {
throw new IllegalStateException("Failed to evaluate expression", ex);
}
}
}

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>{{bootVersion}}</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>app</artifactId>
<version>0.0.1</version>
<packaging>{{packaging}}</packaging>
<properties>
<resourcesJarPath>{{resourcesJarPath}}</resourcesJarPath>
{{containerVersion}}
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-{{container}}</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>resources</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${resourcesJarPath}</systemPath>
</dependency>
{{#additionalDependencies}}
<dependency>
<groupId>{{groupId}}</groupId>
<artifactId>{{artifactId}}</artifactId>
<version>{{version}}</version>
</dependency>
{{/additionalDependencies}}
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -20,9 +20,13 @@ import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarFile;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -86,6 +90,31 @@ public abstract class AbstractEmbeddedServletContainerFactory
return getExplodedWarFileDocumentRoot(getCodeSourceArchive());
}
protected List<URL> getUrlsOfJarsWithMetaInfResources() {
ClassLoader classLoader = getClass().getClassLoader();
List<URL> staticResourceUrls = new ArrayList<URL>();
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) {
staticResourceUrls.add(url);
}
jar.close();
}
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}
return staticResourceUrls;
}
File getExplodedWarFileDocumentRoot(File codeSourceFile) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Code archive: " + codeSourceFile);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* 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.
@ -58,6 +58,7 @@ import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.servlet.ServletMapping;
import org.eclipse.jetty.util.resource.JarResource;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.eclipse.jetty.webapp.AbstractConfiguration;
@ -404,14 +405,21 @@ public class JettyEmbeddedServletContainerFactory
File root = getValidDocumentRoot();
root = (root != null ? root : createTempDir("jetty-docbase"));
try {
if (!root.isDirectory()) {
Resource resource = JarResource
.newJarResource(Resource.newResource(root));
handler.setBaseResource(resource);
}
else {
handler.setBaseResource(Resource.newResource(root.getCanonicalFile()));
List<Resource> resources = new ArrayList<Resource>();
resources.add(
root.isDirectory() ? Resource.newResource(root.getCanonicalFile())
: JarResource.newJarResource(Resource.newResource(root)));
for (URL resourceJarUrl : this.getUrlsOfJarsWithMetaInfResources()) {
Resource resource = Resource
.newResource(resourceJarUrl + "META-INF/resources");
// Jetty 9.2 and earlier do not support nested jars. See
// https://github.com/eclipse/jetty.project/issues/518
if (resource.exists() && resource.isDirectory()) {
resources.add(resource);
}
}
handler.setBaseResource(new ResourceCollection(
resources.toArray(new Resource[resources.size()])));
}
catch (Exception ex) {
throw new IllegalStateException(ex);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2016 the original author or authors.
* 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.
@ -187,7 +187,7 @@ public class TomcatEmbeddedServletContainerFactory
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
File docBase = getValidDocumentRoot();
docBase = (docBase != null ? docBase : createTempDir("tomcat-docbase"));
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
final TomcatEmbeddedContext context = new TomcatEmbeddedContext();
context.setName(getContextPath());
context.setDisplayName(getDisplayName());
context.setPath(getContextPath());
@ -217,6 +217,17 @@ public class TomcatEmbeddedServletContainerFactory
addJasperInitializer(context);
context.addLifecycleListener(new StoreMergedWebXmlListener());
}
context.addLifecycleListener(new LifecycleListener() {
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
TomcatResources.get(context)
.addResourceJars(getUrlsOfJarsWithMetaInfResources());
}
}
});
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
configureContext(context, initializersToUse);
host.addChild(context);
@ -802,7 +813,6 @@ public class TomcatEmbeddedServletContainerFactory
if (servletContext.getAttribute(MERGED_WEB_XML) == null) {
servletContext.setAttribute(MERGED_WEB_XML, getEmptyWebXml());
}
TomcatResources.get(context).addClasspathResources();
}
private String getEmptyWebXml() {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* 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.
@ -16,11 +16,10 @@
package org.springframework.boot.context.embedded.tomcat;
import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
import javax.naming.directory.DirContext;
import javax.servlet.ServletContext;
@ -37,6 +36,7 @@ import org.springframework.util.ReflectionUtils;
*
* @author Dave Syer
* @author Phillip Webb
* @author Andy Wilkinson
*/
abstract class TomcatResources {
@ -46,28 +46,16 @@ abstract class TomcatResources {
this.context = context;
}
/**
* Add resources from the classpath.
*/
public void addClasspathResources() {
ClassLoader loader = getClass().getClassLoader();
if (loader instanceof URLClassLoader) {
for (URL url : ((URLClassLoader) loader).getURLs()) {
String file = url.getFile();
if (file.endsWith(".jar") || file.endsWith(".jar!/")) {
String jar = url.toString();
if (!jar.startsWith("jar:")) {
// A jar file in the file system. Convert to Jar URL.
jar = "jar:" + jar + "!/";
}
addJar(jar);
}
else if (url.toString().startsWith("file:")) {
String dir = url.toString().substring("file:".length());
if (new File(dir).isDirectory()) {
addDir(dir, url);
}
void addResourceJars(List<URL> resourceJarUrls) {
for (URL url : resourceJarUrls) {
String file = url.getFile();
if (file.endsWith(".jar") || file.endsWith(".jar!/")) {
String jar = url.toString();
if (!jar.startsWith("jar:")) {
// A jar file in the file system. Convert to Jar URL.
jar = "jar:" + jar + "!/";
}
addJar(jar);
}
}
}

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.undertow;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import io.undertow.UndertowMessages;
import io.undertow.server.handlers.resource.Resource;
import io.undertow.server.handlers.resource.ResourceChangeListener;
import io.undertow.server.handlers.resource.ResourceManager;
/**
* A {@ResourceManager} that delegates to multiple {@code ResourceManager} instances.
*
* @author Andy Wilkinson
*/
class CompositeResourceManager implements ResourceManager {
private final List<ResourceManager> resourceManagers;
CompositeResourceManager(ResourceManager... resourceManagers) {
this.resourceManagers = Arrays.asList(resourceManagers);
}
@Override
public void close() throws IOException {
for (ResourceManager resourceManager : this.resourceManagers) {
resourceManager.close();
}
}
@Override
public Resource getResource(String path) throws IOException {
for (ResourceManager resourceManager : this.resourceManagers) {
Resource resource = resourceManager.getResource(path);
if (resource != null) {
return resource;
}
}
return null;
}
@Override
public boolean isResourceChangeListenerSupported() {
return false;
}
@Override
public void registerResourceChangeListener(ResourceChangeListener listener) {
throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported();
}
@Override
public void removeResourceChangeListener(ResourceChangeListener listener) {
throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported();
}
}

View File

@ -19,6 +19,7 @@ package org.springframework.boot.context.embedded.undertow;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.Charset;
import java.security.KeyManagementException;
import java.security.KeyStore;
@ -49,7 +50,10 @@ import io.undertow.server.handlers.accesslog.AccessLogHandler;
import io.undertow.server.handlers.accesslog.AccessLogReceiver;
import io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver;
import io.undertow.server.handlers.resource.FileResourceManager;
import io.undertow.server.handlers.resource.Resource;
import io.undertow.server.handlers.resource.ResourceChangeListener;
import io.undertow.server.handlers.resource.ResourceManager;
import io.undertow.server.handlers.resource.URLResource;
import io.undertow.server.session.SessionManager;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
@ -464,13 +468,11 @@ public class UndertowEmbeddedServletContainerFactory
private ResourceManager getDocumentRootResourceManager() {
File root = getCanonicalDocumentRoot();
if (root.isDirectory()) {
return new FileResourceManager(root, 0);
}
if (root.isFile()) {
return new JarResourceManager(root);
}
return ResourceManager.EMPTY_RESOURCE_MANAGER;
List<URL> metaInfResourceJarUrls = getUrlsOfJarsWithMetaInfResources();
ResourceManager rootResourceManager = root.isDirectory()
? new FileResourceManager(root, 0) : new JarResourceManager(root);
return new CompositeResourceManager(rootResourceManager,
new MetaInfResourcesResourceManager(metaInfResourceJarUrls));
}
/**
@ -597,6 +599,49 @@ public class UndertowEmbeddedServletContainerFactory
this.useForwardHeaders = useForwardHeaders;
}
/**
* {@link ResourceManager} that exposes resource in {@code META-INF/resources}
* directory of nested (in {@code BOOT-INF/lib} or {@code WEB-INF/lib}) jars.
*/
private static final class MetaInfResourcesResourceManager
implements ResourceManager {
private final List<URL> metaInfResourceJarUrls;
private MetaInfResourcesResourceManager(List<URL> metaInfResourceJarUrls) {
this.metaInfResourceJarUrls = metaInfResourceJarUrls;
}
@Override
public void close() throws IOException {
}
@Override
public Resource getResource(String path) throws IOException {
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);
}
}
return null;
}
@Override
public boolean isResourceChangeListenerSupported() {
return false;
}
@Override
public void registerResourceChangeListener(ResourceChangeListener listener) {
}
@Override
public void removeResourceChangeListener(ResourceChangeListener listener) {
}
}
/**
* {@link ServletContainerInitializer} to initialize {@link ServletContextInitializer
* ServletContextInitializers}.