diff --git a/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc b/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc index 45fc7ba41849..d05540e9119e 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-executable-jar-format.adoc @@ -283,6 +283,7 @@ the `Main-Class` attribute and leave out `Start-Class`. * `loader.path` can contain directories (scanned recursively for jar and zip files), archive paths, a directory within an archive that is scanned for jar files (for example, `dependencies.jar!/lib`), or wildcard patterns (for the default JVM behavior). + Archive paths can be relative to `loader.home`, or anywhere in the file system with a `jar:file:` prefix. * `loader.path` (if empty) defaults to `BOOT-INF/lib` (meaning a local directory or a nested one if running from an archive). Because of this `PropertiesLauncher` behaves the same as `JarLauncher` when no additional configuration is provided. diff --git a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java index 01253279ac3e..2c6e437df7b8 100755 --- a/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java +++ b/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/PropertiesLauncher.java @@ -25,8 +25,10 @@ import java.net.URLConnection; import java.util.ArrayList; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Properties; +import java.util.Set; import java.util.jar.Manifest; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -299,11 +301,11 @@ private List parsePathsProperty(String commaSeparatedPaths) { List paths = new ArrayList(); for (String path : commaSeparatedPaths.split(",")) { path = cleanupPath(path); - // Empty path (i.e. the archive itself if running from a JAR) is always added - // to the classpath so no need for it to be explicitly listed - if (!path.equals("")) { - paths.add(path); + if ("".equals(path)) { + // This means: user wants root of archive but not current directory + path = "/"; } + paths.add(path); } if (paths.isEmpty()) { paths.add("lib"); @@ -336,7 +338,13 @@ protected String getMainClass() throws Exception { @Override protected ClassLoader createClassLoader(List archives) throws Exception { - ClassLoader loader = super.createClassLoader(archives); + Set urls = new LinkedHashSet(archives.size()); + for (Archive archive : archives) { + urls.add(archive.getUrl()); + } + ClassLoader loader = new LaunchedURLClassLoader(urls.toArray(new URL[0]), + getClass().getClassLoader()); + debug("Classpath: " + urls); String customLoaderClassName = getProperty("loader.classLoader"); if (customLoaderClassName != null) { loader = wrapWithCustomClassLoader(loader, customLoaderClassName); @@ -454,13 +462,15 @@ private List getClassPathArchives(String path) throws Exception { String root = cleanupPath(stripFileUrlPrefix(path)); List lib = new ArrayList(); File file = new File(root); - if (!isAbsolutePath(root)) { - file = new File(this.home, root); - } - if (file.isDirectory()) { - debug("Adding classpath entries from " + file); - Archive archive = new ExplodedArchive(file, false); - lib.add(archive); + if (!"/".equals(root)) { + if (!isAbsolutePath(root)) { + file = new File(this.home, root); + } + if (file.isDirectory()) { + debug("Adding classpath entries from " + file); + Archive archive = new ExplodedArchive(file, false); + lib.add(archive); + } } Archive archive = getArchive(file); if (archive != null) { @@ -488,24 +498,46 @@ private Archive getArchive(File file) throws IOException { return null; } - private List getNestedArchives(String root) throws Exception { - if (root.startsWith("/") + private List getNestedArchives(String path) throws Exception { + String root = path; + if (!root.equals("/") && root.startsWith("/") || this.parent.getUrl().equals(this.home.toURI().toURL())) { // If home dir is same as parent archive, no need to add it twice. return null; } Archive parent = this.parent; - if (root.startsWith("jar:file:") && root.contains("!")) { + if (root.contains("!")) { int index = root.indexOf("!"); - String file = root.substring("jar:file:".length(), index); - parent = new JarFileArchive(new File(file)); + File file = new File(this.home, root.substring(0, index)); + if (root.startsWith("jar:file:")) { + file = new File(root.substring("jar:file:".length(), index)); + } + parent = new JarFileArchive(file); root = root.substring(index + 1, root.length()); while (root.startsWith("/")) { root = root.substring(1); } } + if (root.endsWith(".jar")) { + File file = new File(this.home, root); + if (file.exists()) { + parent = new JarFileArchive(file); + root = ""; + } + } + if (root.equals("/") || root.equals("./") || root.equals(".")) { + // The prefix for nested jars is actually empty if it's at the root + root = ""; + } EntryFilter filter = new PrefixMatchingArchiveFilter(root); - return parent.getNestedArchives(filter); + List archives = new ArrayList(parent.getNestedArchives(filter)); + if (("".equals(root) || ".".equals(root)) && !path.endsWith(".jar") + && parent != this.parent) { + // You can't find the root with an entry filter so it has to be added + // explicitly. But don't add the root of the parent archive. + archives.add(parent); + } + return archives; } private void addNestedEntries(List lib) { @@ -518,7 +550,7 @@ private void addNestedEntries(List lib) { @Override public boolean matches(Entry entry) { if (entry.isDirectory()) { - return entry.getName().startsWith(JarLauncher.BOOT_INF_CLASSES); + return entry.getName().equals(JarLauncher.BOOT_INF_CLASSES); } return entry.getName().startsWith(JarLauncher.BOOT_INF_LIB); } @@ -607,6 +639,9 @@ private PrefixMatchingArchiveFilter(String prefix) { @Override public boolean matches(Entry entry) { + if (entry.isDirectory()) { + return entry.getName().equals(this.prefix); + } return entry.getName().startsWith(this.prefix) && this.filter.matches(entry); } diff --git a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java index 3610844a85af..5ea0678e9ca4 100644 --- a/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java +++ b/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/PropertiesLauncherTests.java @@ -21,12 +21,13 @@ import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.jar.Attributes; import java.util.jar.Manifest; +import org.assertj.core.api.Condition; import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -36,6 +37,9 @@ import org.mockito.MockitoAnnotations; import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.ExplodedArchive; +import org.springframework.boot.loader.archive.JarFileArchive; +import org.springframework.core.io.FileSystemResource; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -72,6 +76,7 @@ public void close() { System.clearProperty("loader.config.name"); System.clearProperty("loader.config.location"); System.clearProperty("loader.system"); + System.clearProperty("loader.classLoader"); } @Test @@ -131,6 +136,21 @@ public void testUserSpecifiedDotPath() throws Exception { .isEqualTo("[.]"); } + @Test + public void testUserSpecifiedSlashPath() throws Exception { + System.setProperty("loader.path", "jars/"); + PropertiesLauncher launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()) + .isEqualTo("[jars/]"); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, new Condition() { + @Override + public boolean matches(Archive value) { + return value.toString().endsWith("app.jar!/"); + } + }); + } + @Test public void testUserSpecifiedWildcardPath() throws Exception { System.setProperty("loader.path", "jars/*"); @@ -153,13 +173,79 @@ public void testUserSpecifiedJarPath() throws Exception { waitFor("Hello World"); } + @Test + public void testUserSpecifiedRootOfJarPath() throws Exception { + System.setProperty("loader.path", + "jar:file:./src/test/resources/nested-jars/app.jar!/"); + PropertiesLauncher launcher = new PropertiesLauncher(); + assertThat(ReflectionTestUtils.getField(launcher, "paths").toString()) + .isEqualTo("[jar:file:./src/test/resources/nested-jars/app.jar!/]"); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, new Condition() { + @Override + public boolean matches(Archive value) { + return value.toString().endsWith("foo.jar!/"); + } + }); + assertThat(archives).areExactly(1, new Condition() { + @Override + public boolean matches(Archive value) { + return value.toString().endsWith("app.jar!/"); + } + }); + } + + @Test + public void testUserSpecifiedRootOfJarPathWithDot() throws Exception { + System.setProperty("loader.path", "nested-jars/app.jar!/./"); + PropertiesLauncher launcher = new PropertiesLauncher(); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, new Condition() { + @Override + public boolean matches(Archive value) { + return value.toString().endsWith("foo.jar!/"); + } + }); + assertThat(archives).areExactly(1, new Condition() { + @Override + public boolean matches(Archive value) { + return value.toString().endsWith("app.jar!/"); + } + }); + } + + @Test + public void testUserSpecifiedRootOfJarPathWithDotAndJarPrefix() throws Exception { + System.setProperty("loader.path", + "jar:file:./src/test/resources/nested-jars/app.jar!/./"); + PropertiesLauncher launcher = new PropertiesLauncher(); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, new Condition() { + @Override + public boolean matches(Archive value) { + return value.toString().endsWith("foo.jar!/"); + } + }); + } + @Test public void testUserSpecifiedJarFileWithNestedArchives() throws Exception { System.setProperty("loader.path", "nested-jars/app.jar"); System.setProperty("loader.main", "demo.Application"); PropertiesLauncher launcher = new PropertiesLauncher(); - launcher.launch(new String[0]); - waitFor("Hello World"); + List archives = launcher.getClassPathArchives(); + assertThat(archives).areExactly(1, new Condition() { + @Override + public boolean matches(Archive value) { + return value.toString().endsWith("foo.jar!/"); + } + }); + assertThat(archives).areExactly(1, new Condition() { + @Override + public boolean matches(Archive value) { + return value.toString().endsWith("app.jar!/"); + } + }); } @Test @@ -209,11 +295,25 @@ public void testUserSpecifiedClassPathOrder() throws Exception { public void testCustomClassLoaderCreation() throws Exception { System.setProperty("loader.classLoader", TestLoader.class.getName()); PropertiesLauncher launcher = new PropertiesLauncher(); - ClassLoader loader = launcher.createClassLoader(Collections.emptyList()); + ClassLoader loader = launcher.createClassLoader(archives()); assertThat(loader).isNotNull(); assertThat(loader.getClass().getName()).isEqualTo(TestLoader.class.getName()); } + private List archives() throws Exception { + List list = new ArrayList(); + String path = System.getProperty("java.class.path"); + for (String url : path.split(File.pathSeparator)) { + if (url.endsWith(".jar")) { + list.add(new JarFileArchive(new FileSystemResource(url).getFile())); + } + else { + list.add(new ExplodedArchive(new FileSystemResource(url).getFile())); + } + } + return list; + } + @Test public void testUserSpecifiedConfigPathWins() throws Exception { diff --git a/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar b/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar index a4365eec85b5..5600ed279efb 100644 Binary files a/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar and b/spring-boot-tools/spring-boot-loader/src/test/resources/nested-jars/app.jar differ