Skip to content

Commit b443b74

Browse files
committed
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 jetty/jetty.project#518 for details). Closes gh-8299
1 parent 21ca1af commit b443b74

18 files changed

+1112
-42
lines changed

spring-boot-integration-tests/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
</properties>
2323
<modules>
2424
<module>spring-boot-devtools-tests</module>
25+
<module>spring-boot-integration-tests-embedded-servlet-container</module>
2526
<module>spring-boot-gradle-tests</module>
2627
<module>spring-boot-launch-script-tests</module>
2728
<module>spring-boot-security-tests</module>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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">
3+
<modelVersion>4.0.0</modelVersion>
4+
<parent>
5+
<groupId>org.springframework.boot</groupId>
6+
<artifactId>spring-boot-integration-tests</artifactId>
7+
<version>1.4.5.BUILD-SNAPSHOT</version>
8+
</parent>
9+
<artifactId>spring-boot-integration-tests-embedded-servlet-container</artifactId>
10+
<packaging>jar</packaging>
11+
<name>Spring Boot Embedded Servlet Container Integration Tests</name>
12+
<url>http://projects.spring.io/spring-boot/</url>
13+
<organization>
14+
<name>Pivotal Software, Inc.</name>
15+
<url>http://www.spring.io</url>
16+
</organization>
17+
<properties>
18+
<main.basedir>${basedir}/../..</main.basedir>
19+
</properties>
20+
<dependencies>
21+
<dependency>
22+
<groupId>org.springframework.boot</groupId>
23+
<artifactId>spring-boot-starter-logging</artifactId>
24+
<scope>test</scope>
25+
</dependency>
26+
<dependency>
27+
<groupId>org.springframework.boot</groupId>
28+
<artifactId>spring-boot-starter-test</artifactId>
29+
<scope>test</scope>
30+
</dependency>
31+
<dependency>
32+
<groupId>com.samskivert</groupId>
33+
<artifactId>jmustache</artifactId>
34+
<scope>test</scope>
35+
</dependency>
36+
<dependency>
37+
<groupId>javax.servlet</groupId>
38+
<artifactId>javax.servlet-api</artifactId>
39+
<scope>test</scope>
40+
</dependency>
41+
<dependency>
42+
<groupId>org.apache.maven.shared</groupId>
43+
<artifactId>maven-invoker</artifactId>
44+
<version>3.0.0</version>
45+
<scope>test</scope>
46+
</dependency>
47+
<dependency>
48+
<groupId>org.springframework</groupId>
49+
<artifactId>spring-web</artifactId>
50+
<scope>test</scope>
51+
</dependency>
52+
</dependencies>
53+
<build>
54+
<plugins>
55+
<plugin>
56+
<groupId>org.apache.maven.plugins</groupId>
57+
<artifactId>maven-surefire-plugin</artifactId>
58+
<configuration>
59+
<systemPropertyVariables>
60+
<maven.home>${maven.home}</maven.home>
61+
</systemPropertyVariables>
62+
</configuration>
63+
</plugin>
64+
</plugins>
65+
</build>
66+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example;
18+
19+
import java.io.IOException;
20+
import java.net.URL;
21+
22+
import javax.servlet.ServletException;
23+
import javax.servlet.http.HttpServlet;
24+
import javax.servlet.http.HttpServletRequest;
25+
import javax.servlet.http.HttpServletResponse;
26+
27+
import org.springframework.boot.autoconfigure.SpringBootApplication;
28+
import org.springframework.boot.builder.SpringApplicationBuilder;
29+
import org.springframework.boot.system.EmbeddedServerPortFileWriter;
30+
import org.springframework.boot.web.servlet.ServletRegistrationBean;
31+
import org.springframework.context.annotation.Bean;
32+
33+
/**
34+
* Test application for verifying an embedded container's static resource handling.
35+
*
36+
* @author Andy Wilkinson
37+
*/
38+
@SpringBootApplication
39+
public class ResourceHandlingApplication {
40+
41+
public static void main(String[] args) {
42+
new SpringApplicationBuilder(ResourceHandlingApplication.class)
43+
.properties("server.port:0")
44+
.listeners(new EmbeddedServerPortFileWriter("target/server.port"))
45+
.run(args);
46+
}
47+
48+
@Bean
49+
public ServletRegistrationBean resourceServletRegistration() {
50+
ServletRegistrationBean registration = new ServletRegistrationBean(
51+
new HttpServlet() {
52+
53+
@Override
54+
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
55+
throws ServletException, IOException {
56+
URL resource = getServletContext()
57+
.getResource(req.getQueryString());
58+
if (resource == null) {
59+
resp.sendError(404);
60+
}
61+
else {
62+
resp.getWriter().println(resource);
63+
resp.getWriter().flush();
64+
}
65+
}
66+
67+
});
68+
registration.addUrlMappings("/servletContext");
69+
return registration;
70+
}
71+
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.embedded;
18+
19+
import java.io.File;
20+
import java.io.FileReader;
21+
import java.lang.ProcessBuilder.Redirect;
22+
import java.util.ArrayList;
23+
import java.util.List;
24+
25+
import org.junit.rules.ExternalResource;
26+
27+
import org.springframework.util.FileCopyUtils;
28+
29+
/**
30+
* Base {@link ExternalResource} for launching a Spring Boot application as part of a
31+
* JUnit test.
32+
*
33+
* @author Andy Wilkinson
34+
*/
35+
abstract class AbstractApplicationLauncher extends ExternalResource {
36+
37+
private final File serverPortFile = new File("target/server.port");
38+
39+
private final ApplicationBuilder applicationBuilder;
40+
41+
private Process process;
42+
43+
private int httpPort;
44+
45+
protected AbstractApplicationLauncher(ApplicationBuilder applicationBuilder) {
46+
this.applicationBuilder = applicationBuilder;
47+
}
48+
49+
@Override
50+
protected final void before() throws Throwable {
51+
this.process = startApplication();
52+
}
53+
54+
@Override
55+
protected final void after() {
56+
this.process.destroy();
57+
}
58+
59+
public final int getHttpPort() {
60+
return this.httpPort;
61+
}
62+
63+
protected abstract List<String> getArguments(File archive);
64+
65+
private Process startApplication() throws Exception {
66+
this.serverPortFile.delete();
67+
File archive = this.applicationBuilder.buildApplication();
68+
List<String> arguments = new ArrayList<String>();
69+
arguments.add(System.getProperty("java.home") + "/bin/java");
70+
arguments.addAll(getArguments(archive));
71+
ProcessBuilder processBuilder = new ProcessBuilder(
72+
arguments.toArray(new String[arguments.size()]));
73+
processBuilder.redirectOutput(Redirect.INHERIT);
74+
processBuilder.redirectError(Redirect.INHERIT);
75+
Process process = processBuilder.start();
76+
this.httpPort = awaitServerPort(process);
77+
return process;
78+
}
79+
80+
private int awaitServerPort(Process process) throws Exception {
81+
long end = System.currentTimeMillis() + 30000;
82+
while (this.serverPortFile.length() == 0) {
83+
if (System.currentTimeMillis() > end) {
84+
throw new IllegalStateException(
85+
"server.port file was not written within 30 seconds");
86+
}
87+
if (!process.isAlive()) {
88+
throw new IllegalStateException("Application failed to launch");
89+
}
90+
Thread.sleep(100);
91+
}
92+
return Integer.parseInt(
93+
FileCopyUtils.copyToString(new FileReader(this.serverPortFile)));
94+
}
95+
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.embedded;
18+
19+
import java.io.IOException;
20+
import java.net.URI;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Map;
24+
25+
import org.codehaus.plexus.util.StringUtils;
26+
import org.junit.ClassRule;
27+
import org.junit.Rule;
28+
import org.junit.rules.TemporaryFolder;
29+
30+
import org.springframework.http.client.ClientHttpResponse;
31+
import org.springframework.web.client.ResponseErrorHandler;
32+
import org.springframework.web.client.RestTemplate;
33+
import org.springframework.web.util.UriTemplateHandler;
34+
35+
/**
36+
* Base class for embedded servlet container integration tests.
37+
*
38+
* @author Andy Wilkinson
39+
*/
40+
public abstract class AbstractEmbeddedServletContainerIntegrationTests {
41+
42+
@ClassRule
43+
public static final TemporaryFolder temporaryFolder = new TemporaryFolder();
44+
45+
@Rule
46+
public final AbstractApplicationLauncher launcher;
47+
48+
protected final RestTemplate rest = new RestTemplate();
49+
50+
public static Object[] parameters(String packaging) {
51+
List<Object> parameters = new ArrayList<Object>();
52+
parameters.addAll(createParameters(packaging, "jetty", "current"));
53+
parameters.addAll(
54+
createParameters(packaging, "tomcat", "current", "8.0.41", "7.0.75"));
55+
parameters.addAll(createParameters(packaging, "undertow", "current"));
56+
return parameters.toArray(new Object[parameters.size()]);
57+
}
58+
59+
private static List<Object> createParameters(String packaging, String container,
60+
String... versions) {
61+
List<Object> parameters = new ArrayList<Object>();
62+
for (String version : versions) {
63+
ApplicationBuilder applicationBuilder = new ApplicationBuilder(
64+
temporaryFolder, packaging, container, version);
65+
parameters.add(new Object[] {
66+
StringUtils.capitalise(container) + " " + version + " packaged "
67+
+ packaging,
68+
new PackagedApplicationLauncher(applicationBuilder) });
69+
parameters.add(new Object[] {
70+
StringUtils.capitalise(container) + " " + version + " exploded "
71+
+ packaging,
72+
new ExplodedApplicationLauncher(applicationBuilder) });
73+
}
74+
return parameters;
75+
}
76+
77+
protected AbstractEmbeddedServletContainerIntegrationTests(String name,
78+
AbstractApplicationLauncher launcher) {
79+
this.launcher = launcher;
80+
this.rest.setErrorHandler(new ResponseErrorHandler() {
81+
82+
@Override
83+
public boolean hasError(ClientHttpResponse response) throws IOException {
84+
return false;
85+
}
86+
87+
@Override
88+
public void handleError(ClientHttpResponse response) throws IOException {
89+
90+
}
91+
92+
});
93+
this.rest.setUriTemplateHandler(new UriTemplateHandler() {
94+
95+
@Override
96+
public URI expand(String uriTemplate, Object... uriVariables) {
97+
return URI.create(
98+
"http://localhost:" + launcher.getHttpPort() + uriTemplate);
99+
}
100+
101+
@Override
102+
public URI expand(String uriTemplate, Map<String, ?> uriVariables) {
103+
return URI.create(
104+
"http://localhost:" + launcher.getHttpPort() + uriTemplate);
105+
}
106+
107+
});
108+
}
109+
110+
}

0 commit comments

Comments
 (0)