Skip to content

Commit 14638e6

Browse files
Dave Syerphilwebb
authored andcommitted
Extended PropertiesLauncher class location logic
Update `PropertiesLauncher` so that classes can be loaded outside of `BOOT-INF/classes`. You can use a subdirectory, or the root directory of an external jar (but not the parent archive to avoid issues with agents and awkward delegation models). Fixes gh-8480 Closes gh-8486
1 parent 5abc050 commit 14638e6

File tree

6 files changed

+136
-28
lines changed

6 files changed

+136
-28
lines changed

spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ static Validator mvcValidator() {
735735
static class DelegatingWebMvcValidator extends DelegatingValidator
736736
implements ApplicationContextAware, InitializingBean, DisposableBean {
737737

738-
public DelegatingWebMvcValidator(Validator targetValidator) {
738+
DelegatingWebMvcValidator(Validator targetValidator) {
739739
super(targetValidator);
740740
}
741741

spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ public void validateWithHintsShouldDelegateToValidator() throws Exception {
9999
Object[] hints = { "foo", "bar" };
100100
this.delegating.validate(target, errors, hints);
101101
verify(this.delegate).validate(target, errors, hints);
102-
;
103102
}
104103

105104
@Test

spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,8 @@ the `Main-Class` attribute and leave out `Start-Class`.
283283
* `loader.path` can contain directories (scanned recursively for jar and zip files),
284284
archive paths, a directory within an archive that is scanned for jar files (for
285285
example, `dependencies.jar!/lib`), or wildcard patterns (for the default JVM behavior).
286+
Archive paths can be relative to `loader.home`, or anywhere in the file system with a
287+
`jar:file:` prefix.
286288
* `loader.path` (if empty) defaults to `BOOT-INF/lib` (meaning a local directory or a
287289
nested one if running from an archive). Because of this `PropertiesLauncher` behaves the
288290
same as `JarLauncher` when no additional configuration is provided.

spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
import java.net.URLConnection;
2626
import java.util.ArrayList;
2727
import java.util.Collections;
28+
import java.util.LinkedHashSet;
2829
import java.util.List;
2930
import java.util.Properties;
31+
import java.util.Set;
3032
import java.util.jar.Manifest;
3133
import java.util.regex.Matcher;
3234
import java.util.regex.Pattern;
@@ -299,11 +301,9 @@ private List<String> parsePathsProperty(String commaSeparatedPaths) {
299301
List<String> paths = new ArrayList<String>();
300302
for (String path : commaSeparatedPaths.split(",")) {
301303
path = cleanupPath(path);
302-
// Empty path (i.e. the archive itself if running from a JAR) is always added
303-
// to the classpath so no need for it to be explicitly listed
304-
if (!path.equals("")) {
305-
paths.add(path);
306-
}
304+
// "" means the user wants root of archive but not current directory
305+
path = ("".equals(path) ? "/" : path);
306+
paths.add(path);
307307
}
308308
if (paths.isEmpty()) {
309309
paths.add("lib");
@@ -336,7 +336,13 @@ protected String getMainClass() throws Exception {
336336

337337
@Override
338338
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
339-
ClassLoader loader = super.createClassLoader(archives);
339+
Set<URL> urls = new LinkedHashSet<URL>(archives.size());
340+
for (Archive archive : archives) {
341+
urls.add(archive.getUrl());
342+
}
343+
ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(new URL[0]),
344+
getClass().getClassLoader());
345+
debug("Classpath: " + urls);
340346
String customLoaderClassName = getProperty("loader.classLoader");
341347
if (customLoaderClassName != null) {
342348
loader = wrapWithCustomClassLoader(loader, customLoaderClassName);
@@ -454,13 +460,15 @@ private List<Archive> getClassPathArchives(String path) throws Exception {
454460
String root = cleanupPath(stripFileUrlPrefix(path));
455461
List<Archive> lib = new ArrayList<Archive>();
456462
File file = new File(root);
457-
if (!isAbsolutePath(root)) {
458-
file = new File(this.home, root);
459-
}
460-
if (file.isDirectory()) {
461-
debug("Adding classpath entries from " + file);
462-
Archive archive = new ExplodedArchive(file, false);
463-
lib.add(archive);
463+
if (!"/".equals(root)) {
464+
if (!isAbsolutePath(root)) {
465+
file = new File(this.home, root);
466+
}
467+
if (file.isDirectory()) {
468+
debug("Adding classpath entries from " + file);
469+
Archive archive = new ExplodedArchive(file, false);
470+
lib.add(archive);
471+
}
464472
}
465473
Archive archive = getArchive(file);
466474
if (archive != null) {
@@ -488,24 +496,46 @@ private Archive getArchive(File file) throws IOException {
488496
return null;
489497
}
490498

491-
private List<Archive> getNestedArchives(String root) throws Exception {
492-
if (root.startsWith("/")
493-
|| this.parent.getUrl().equals(this.home.toURI().toURL())) {
499+
private List<Archive> getNestedArchives(String path) throws Exception {
500+
Archive parent = this.parent;
501+
String root = path;
502+
if (!root.equals("/") && root.startsWith("/")
503+
|| parent.getUrl().equals(this.home.toURI().toURL())) {
494504
// If home dir is same as parent archive, no need to add it twice.
495505
return null;
496506
}
497-
Archive parent = this.parent;
498-
if (root.startsWith("jar:file:") && root.contains("!")) {
507+
if (root.contains("!")) {
499508
int index = root.indexOf("!");
500-
String file = root.substring("jar:file:".length(), index);
501-
parent = new JarFileArchive(new File(file));
509+
File file = new File(this.home, root.substring(0, index));
510+
if (root.startsWith("jar:file:")) {
511+
file = new File(root.substring("jar:file:".length(), index));
512+
}
513+
parent = new JarFileArchive(file);
502514
root = root.substring(index + 1, root.length());
503515
while (root.startsWith("/")) {
504516
root = root.substring(1);
505517
}
506518
}
519+
if (root.endsWith(".jar")) {
520+
File file = new File(this.home, root);
521+
if (file.exists()) {
522+
parent = new JarFileArchive(file);
523+
root = "";
524+
}
525+
}
526+
if (root.equals("/") || root.equals("./") || root.equals(".")) {
527+
// The prefix for nested jars is actually empty if it's at the root
528+
root = "";
529+
}
507530
EntryFilter filter = new PrefixMatchingArchiveFilter(root);
508-
return parent.getNestedArchives(filter);
531+
List<Archive> archives = new ArrayList<Archive>(parent.getNestedArchives(filter));
532+
if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar")
533+
&& parent != this.parent) {
534+
// You can't find the root with an entry filter so it has to be added
535+
// explicitly. But don't add the root of the parent archive.
536+
archives.add(parent);
537+
}
538+
return archives;
509539
}
510540

511541
private void addNestedEntries(List<Archive> lib) {
@@ -518,7 +548,7 @@ private void addNestedEntries(List<Archive> lib) {
518548
@Override
519549
public boolean matches(Entry entry) {
520550
if (entry.isDirectory()) {
521-
return entry.getName().startsWith(JarLauncher.BOOT_INF_CLASSES);
551+
return entry.getName().equals(JarLauncher.BOOT_INF_CLASSES);
522552
}
523553
return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB);
524554
}
@@ -607,6 +637,9 @@ private PrefixMatchingArchiveFilter(String prefix) {
607637

608638
@Override
609639
public boolean matches(Entry entry) {
640+
if (entry.isDirectory()) {
641+
return entry.getName().equals(this.prefix);
642+
}
610643
return entry.getName().startsWith(this.prefix) && this.filter.matches(entry);
611644
}
612645

spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@
2121
import java.io.IOException;
2222
import java.net.URL;
2323
import java.net.URLClassLoader;
24+
import java.util.ArrayList;
2425
import java.util.Arrays;
25-
import java.util.Collections;
2626
import java.util.List;
2727
import java.util.jar.Attributes;
2828
import java.util.jar.Manifest;
2929

30+
import org.assertj.core.api.Condition;
3031
import org.junit.After;
3132
import org.junit.Before;
3233
import org.junit.Rule;
@@ -36,6 +37,9 @@
3637
import org.mockito.MockitoAnnotations;
3738

3839
import org.springframework.boot.loader.archive.Archive;
40+
import org.springframework.boot.loader.archive.ExplodedArchive;
41+
import org.springframework.boot.loader.archive.JarFileArchive;
42+
import org.springframework.core.io.FileSystemResource;
3943
import org.springframework.test.util.ReflectionTestUtils;
4044

4145
import static org.assertj.core.api.Assertions.assertThat;
@@ -72,6 +76,7 @@ public void close() {
7276
System.clearProperty("loader.config.name");
7377
System.clearProperty("loader.config.location");
7478
System.clearProperty("loader.system");
79+
System.clearProperty("loader.classLoader");
7580
}
7681

7782
@Test
@@ -131,6 +136,16 @@ public void testUserSpecifiedDotPath() throws Exception {
131136
.isEqualTo("[.]");
132137
}
133138

139+
@Test
140+
public void testUserSpecifiedSlashPath() throws Exception {
141+
System.setProperty("loader.path", "jars/");
142+
PropertiesLauncher launcher = new PropertiesLauncher();
143+
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
144+
.isEqualTo("[jars/]");
145+
List<Archive> archives = launcher.getClassPathArchives();
146+
assertThat(archives).areExactly(1, endingWith("app.jar!/"));
147+
}
148+
134149
@Test
135150
public void testUserSpecifiedWildcardPath() throws Exception {
136151
System.setProperty("loader.path", "jars/*");
@@ -153,13 +168,44 @@ public void testUserSpecifiedJarPath() throws Exception {
153168
waitFor("Hello World");
154169
}
155170

171+
@Test
172+
public void testUserSpecifiedRootOfJarPath() throws Exception {
173+
System.setProperty("loader.path",
174+
"jar:file:./src/test/resources/nested-jars/app.jar!/");
175+
PropertiesLauncher launcher = new PropertiesLauncher();
176+
assertThat(ReflectionTestUtils.getField(launcher, "paths").toString())
177+
.isEqualTo("[jar:file:./src/test/resources/nested-jars/app.jar!/]");
178+
List<Archive> archives = launcher.getClassPathArchives();
179+
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
180+
assertThat(archives).areExactly(1, endingWith("app.jar!/"));
181+
}
182+
183+
@Test
184+
public void testUserSpecifiedRootOfJarPathWithDot() throws Exception {
185+
System.setProperty("loader.path", "nested-jars/app.jar!/./");
186+
PropertiesLauncher launcher = new PropertiesLauncher();
187+
List<Archive> archives = launcher.getClassPathArchives();
188+
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
189+
assertThat(archives).areExactly(1, endingWith("app.jar!/"));
190+
}
191+
192+
@Test
193+
public void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception {
194+
System.setProperty("loader.path",
195+
"jar:file:./src/test/resources/nested-jars/app.jar!/./");
196+
PropertiesLauncher launcher = new PropertiesLauncher();
197+
List<Archive> archives = launcher.getClassPathArchives();
198+
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
199+
}
200+
156201
@Test
157202
public void testUserSpecifiedJarFileWithNestedArchives() throws Exception {
158203
System.setProperty("loader.path", "nested-jars/app.jar");
159204
System.setProperty("loader.main", "demo.Application");
160205
PropertiesLauncher launcher = new PropertiesLauncher();
161-
launcher.launch(new String[0]);
162-
waitFor("Hello World");
206+
List<Archive> archives = launcher.getClassPathArchives();
207+
assertThat(archives).areExactly(1, endingWith("foo.jar!/"));
208+
assertThat(archives).areExactly(1, endingWith("app.jar!/"));
163209
}
164210

165211
@Test
@@ -209,11 +255,28 @@ public void testUserSpecifiedClassPathOrder() throws Exception {
209255
public void testCustomClassLoaderCreation() throws Exception {
210256
System.setProperty("loader.classLoader", TestLoader.class.getName());
211257
PropertiesLauncher launcher = new PropertiesLauncher();
212-
ClassLoader loader = launcher.createClassLoader(Collections.<Archive>emptyList());
258+
ClassLoader loader = launcher.createClassLoader(archives());
213259
assertThat(loader).isNotNull();
214260
assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName());
215261
}
216262

263+
private List<Archive> archives() throws Exception {
264+
List<Archive> archives = new ArrayList<Archive>();
265+
String path = System.getProperty("java.class.path");
266+
for (String url : path.split(File.pathSeparator)) {
267+
archives.add(archive(url));
268+
}
269+
return archives;
270+
}
271+
272+
private Archive archive(String url) throws IOException {
273+
File file = new FileSystemResource(url).getFile();
274+
if (url.endsWith(".jar")) {
275+
return new JarFileArchive(file);
276+
}
277+
return new ExplodedArchive(file);
278+
}
279+
217280
@Test
218281
public void testUserSpecifiedConfigPathWins() throws Exception {
219282

@@ -280,6 +343,17 @@ private void waitFor(String value) throws Exception {
280343
assertThat(timeout).as("Timed out waiting for (" + value + ")").isTrue();
281344
}
282345

346+
private Condition<Archive> endingWith(final String value) {
347+
return new Condition<Archive>() {
348+
349+
@Override
350+
public boolean matches(Archive archive) {
351+
return archive.toString().endsWith(value);
352+
}
353+
354+
};
355+
}
356+
283357
public static class TestLoader extends URLClassLoader {
284358

285359
public TestLoader(ClassLoader parent) {
Binary file not shown.

0 commit comments

Comments
 (0)