Skip to content

Commit a2cf045

Browse files
committed
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.
1 parent 67068fc commit a2cf045

14 files changed

+604
-45
lines changed

spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractApplicationLauncher.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@
3434
*/
3535
abstract class AbstractApplicationLauncher extends ExternalResource {
3636

37-
private final File serverPortFile = new File("target/server.port");
38-
3937
private final ApplicationBuilder applicationBuilder;
4038

4139
private Process process;
@@ -62,8 +60,15 @@ public final int getHttpPort() {
6260

6361
protected abstract List<String> getArguments(File archive);
6462

63+
protected abstract File getWorkingDirectory();
64+
65+
protected abstract String getDescription(String packaging);
66+
6567
private Process startApplication() throws Exception {
66-
this.serverPortFile.delete();
68+
File workingDirectory = getWorkingDirectory();
69+
File serverPortFile = workingDirectory == null ? new File("target/server.port")
70+
: new File(workingDirectory, "target/server.port");
71+
serverPortFile.delete();
6772
File archive = this.applicationBuilder.buildApplication();
6873
List<String> arguments = new ArrayList<String>();
6974
arguments.add(System.getProperty("java.home") + "/bin/java");
@@ -72,14 +77,17 @@ private Process startApplication() throws Exception {
7277
arguments.toArray(new String[arguments.size()]));
7378
processBuilder.redirectOutput(Redirect.INHERIT);
7479
processBuilder.redirectError(Redirect.INHERIT);
80+
if (workingDirectory != null) {
81+
processBuilder.directory(workingDirectory);
82+
}
7583
Process process = processBuilder.start();
76-
this.httpPort = awaitServerPort(process);
84+
this.httpPort = awaitServerPort(process, serverPortFile);
7785
return process;
7886
}
7987

80-
private int awaitServerPort(Process process) throws Exception {
88+
private int awaitServerPort(Process process, File serverPortFile) throws Exception {
8189
long end = System.currentTimeMillis() + 30000;
82-
while (this.serverPortFile.length() == 0) {
90+
while (serverPortFile.length() == 0) {
8391
if (System.currentTimeMillis() > end) {
8492
throw new IllegalStateException(
8593
"server.port file was not written within 30 seconds");
@@ -89,8 +97,8 @@ private int awaitServerPort(Process process) throws Exception {
8997
}
9098
Thread.sleep(100);
9199
}
92-
return Integer.parseInt(
93-
FileCopyUtils.copyToString(new FileReader(this.serverPortFile)));
100+
return Integer
101+
.parseInt(FileCopyUtils.copyToString(new FileReader(serverPortFile)));
94102
}
95103

96104
}

spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/AbstractEmbeddedServletContainerIntegrationTests.java

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.io.IOException;
2020
import java.net.URI;
2121
import java.util.ArrayList;
22+
import java.util.Arrays;
23+
import java.util.Collections;
2224
import java.util.List;
2325
import java.util.Map;
2426

@@ -47,29 +49,38 @@ public abstract class AbstractEmbeddedServletContainerIntegrationTests {
4749

4850
protected final RestTemplate rest = new RestTemplate();
4951

50-
public static Object[] parameters(String packaging) {
52+
public static Object[] parameters(String packaging,
53+
List<Class<? extends AbstractApplicationLauncher>> applicationLaunchers) {
5154
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"));
55+
parameters.addAll(createParameters(packaging, "jetty",
56+
Collections.singletonList("current"), applicationLaunchers));
57+
parameters.addAll(createParameters(packaging, "tomcat",
58+
Arrays.asList("current", "8.0.41", "7.0.75"), applicationLaunchers));
59+
parameters.addAll(createParameters(packaging, "undertow",
60+
Collections.singletonList("current"), applicationLaunchers));
5661
return parameters.toArray(new Object[parameters.size()]);
5762
}
5863

5964
private static List<Object> createParameters(String packaging, String container,
60-
String... versions) {
65+
List<String> versions,
66+
List<Class<? extends AbstractApplicationLauncher>> applicationLaunchers) {
6167
List<Object> parameters = new ArrayList<Object>();
6268
for (String version : versions) {
6369
ApplicationBuilder applicationBuilder = new ApplicationBuilder(
6470
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) });
71+
for (Class<? extends AbstractApplicationLauncher> launcherClass : applicationLaunchers) {
72+
try {
73+
AbstractApplicationLauncher launcher = launcherClass
74+
.getDeclaredConstructor(ApplicationBuilder.class)
75+
.newInstance(applicationBuilder);
76+
String name = StringUtils.capitalise(container) + " " + version + ": "
77+
+ launcher.getDescription(packaging);
78+
parameters.add(new Object[] { name, launcher });
79+
}
80+
catch (Exception ex) {
81+
throw new RuntimeException(ex);
82+
}
83+
}
7384
}
7485
return parameters;
7586
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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.FileOutputStream;
21+
import java.io.IOException;
22+
import java.util.ArrayList;
23+
import java.util.Arrays;
24+
import java.util.Collections;
25+
import java.util.Enumeration;
26+
import java.util.List;
27+
import java.util.jar.JarEntry;
28+
import java.util.jar.JarFile;
29+
30+
import org.springframework.util.FileSystemUtils;
31+
import org.springframework.util.StreamUtils;
32+
import org.springframework.util.StringUtils;
33+
34+
/**
35+
* {@link AbstractApplicationLauncher} that launches a Spring Boot application with a
36+
* classpath similar to that used when run with Maven or Gradle.
37+
*
38+
* @author Andy Wilkinson
39+
*/
40+
class BootRunApplicationLauncher extends AbstractApplicationLauncher {
41+
42+
private final File exploded = new File("target/run");
43+
44+
BootRunApplicationLauncher(ApplicationBuilder applicationBuilder) {
45+
super(applicationBuilder);
46+
}
47+
48+
@Override
49+
protected List<String> getArguments(File archive) {
50+
try {
51+
explodeArchive(archive);
52+
deleteLauncherClasses();
53+
File targetClasses = populateTargetClasses(archive);
54+
File dependencies = populateDependencies(archive);
55+
populateSrcMainWebapp();
56+
List<String> classpath = new ArrayList<String>();
57+
classpath.add(targetClasses.getAbsolutePath());
58+
for (File dependency : dependencies.listFiles()) {
59+
classpath.add(dependency.getAbsolutePath());
60+
}
61+
return Arrays.asList("-cp",
62+
StringUtils.collectionToDelimitedString(classpath,
63+
File.pathSeparator),
64+
"com.example.ResourceHandlingApplication");
65+
}
66+
catch (IOException ex) {
67+
throw new RuntimeException(ex);
68+
}
69+
}
70+
71+
private void deleteLauncherClasses() {
72+
FileSystemUtils.deleteRecursively(new File(this.exploded, "org"));
73+
}
74+
75+
private File populateTargetClasses(File archive) {
76+
File targetClasses = new File(this.exploded, "target/classes");
77+
targetClasses.mkdirs();
78+
new File(this.exploded, getClassesPath(archive)).renameTo(targetClasses);
79+
return targetClasses;
80+
}
81+
82+
private File populateDependencies(File archive) {
83+
File dependencies = new File(this.exploded, "dependencies");
84+
dependencies.mkdirs();
85+
List<String> libPaths = getLibPaths(archive);
86+
for (String libPath : libPaths) {
87+
for (File jar : new File(this.exploded, libPath).listFiles()) {
88+
jar.renameTo(new File(dependencies, jar.getName()));
89+
}
90+
}
91+
return dependencies;
92+
}
93+
94+
private void populateSrcMainWebapp() {
95+
File srcMainWebapp = new File(this.exploded, "src/main/webapp");
96+
srcMainWebapp.mkdirs();
97+
new File(this.exploded, "webapp-resource.txt")
98+
.renameTo(new File(srcMainWebapp, "webapp-resource.txt"));
99+
}
100+
101+
private String getClassesPath(File archive) {
102+
return archive.getName().endsWith(".jar") ? "BOOT-INF/classes"
103+
: "WEB-INF/classes";
104+
}
105+
106+
private List<String> getLibPaths(File archive) {
107+
return archive.getName().endsWith(".jar")
108+
? Collections.singletonList("BOOT-INF/lib")
109+
: Arrays.asList("WEB-INF/lib", "WEB-INF/lib-provided");
110+
}
111+
112+
private void explodeArchive(File archive) throws IOException {
113+
FileSystemUtils.deleteRecursively(this.exploded);
114+
JarFile jarFile = new JarFile(archive);
115+
Enumeration<JarEntry> entries = jarFile.entries();
116+
while (entries.hasMoreElements()) {
117+
JarEntry jarEntry = entries.nextElement();
118+
File extracted = new File(this.exploded, jarEntry.getName());
119+
if (jarEntry.isDirectory()) {
120+
extracted.mkdirs();
121+
}
122+
else {
123+
FileOutputStream extractedOutputStream = new FileOutputStream(extracted);
124+
StreamUtils.copy(jarFile.getInputStream(jarEntry), extractedOutputStream);
125+
extractedOutputStream.close();
126+
}
127+
}
128+
jarFile.close();
129+
}
130+
131+
@Override
132+
protected File getWorkingDirectory() {
133+
return this.exploded;
134+
}
135+
136+
@Override
137+
protected String getDescription(String packaging) {
138+
return "build system run " + packaging + " project";
139+
}
140+
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.util.Arrays;
20+
21+
import org.junit.Test;
22+
import org.junit.runner.RunWith;
23+
import org.junit.runners.Parameterized;
24+
import org.junit.runners.Parameterized.Parameters;
25+
26+
import org.springframework.http.HttpStatus;
27+
import org.springframework.http.ResponseEntity;
28+
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
/**
32+
* Integration tests for Spring Boot's embedded servlet container support when developing
33+
* a jar application.
34+
*
35+
* @author Andy Wilkinson
36+
*/
37+
@RunWith(Parameterized.class)
38+
public class EmbeddedServletContainerJarDevelopmentIntegrationTests
39+
extends AbstractEmbeddedServletContainerIntegrationTests {
40+
41+
@Parameters(name = "{0}")
42+
public static Object[] parameters() {
43+
return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar", Arrays
44+
.asList(BootRunApplicationLauncher.class, IdeApplicationLauncher.class));
45+
}
46+
47+
public EmbeddedServletContainerJarDevelopmentIntegrationTests(String name,
48+
AbstractApplicationLauncher launcher) {
49+
super(name, launcher);
50+
}
51+
52+
@Test
53+
public void metaInfResourceFromDependencyIsAvailableViaHttp() throws Exception {
54+
ResponseEntity<String> entity = this.rest
55+
.getForEntity("/nested-meta-inf-resource.txt", String.class);
56+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
57+
}
58+
59+
@Test
60+
public void metaInfResourceFromDependencyIsAvailableViaServletContext()
61+
throws Exception {
62+
ResponseEntity<String> entity = this.rest
63+
.getForEntity("/nested-meta-inf-resource.txt", String.class);
64+
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
65+
}
66+
67+
}

spring-boot-integration-tests/spring-boot-integration-tests-embedded-servlet-container/src/test/java/org/springframework/boot/context/embedded/EmbeddedServletContainerJarPackagingIntegrationTests.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.boot.context.embedded;
1818

19+
import java.util.Arrays;
20+
1921
import org.junit.Test;
2022
import org.junit.runner.RunWith;
2123
import org.junit.runners.Parameterized;
@@ -38,7 +40,9 @@ public class EmbeddedServletContainerJarPackagingIntegrationTests
3840

3941
@Parameters(name = "{0}")
4042
public static Object[] parameters() {
41-
return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar");
43+
return AbstractEmbeddedServletContainerIntegrationTests.parameters("jar",
44+
Arrays.asList(PackagedApplicationLauncher.class,
45+
ExplodedApplicationLauncher.class));
4246
}
4347

4448
public EmbeddedServletContainerJarPackagingIntegrationTests(String name,

0 commit comments

Comments
 (0)