diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java index f08693eaca..a51f65a06d 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration.java @@ -735,7 +735,7 @@ public class WebMvcAutoConfiguration { static class DelegatingWebMvcValidator extends DelegatingValidator implements ApplicationContextAware, InitializingBean, DisposableBean { - public DelegatingWebMvcValidator(Validator targetValidator) { + DelegatingWebMvcValidator(Validator targetValidator) { super(targetValidator); } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java index 3ced1d921e..04cc4e8c07 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/validation/DelegatingValidatorTests.java @@ -99,7 +99,6 @@ public class DelegatingValidatorTests { Object[] hints = { "foo", "bar" }; this.delegating.validate(target, errors, hints); verify(this.delegate).validate(target, errors, hints); - ; } @Test 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 45fc7ba418..7d8ac458eb 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,8 @@ 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 01253279ac..395af6eea8 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.URL; 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,9 @@ public class PropertiesLauncher extends Launcher { 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); - } + // "" means the user wants root of archive but not current directory + path = ("".equals(path) ? "/" : path); + paths.add(path); } if (paths.isEmpty()) { paths.add("lib"); @@ -336,7 +336,13 @@ public class PropertiesLauncher extends Launcher { @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 +460,15 @@ public class PropertiesLauncher extends Launcher { 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 +496,46 @@ public class PropertiesLauncher extends Launcher { return null; } - private List getNestedArchives(String root) throws Exception { - if (root.startsWith("/") - || this.parent.getUrl().equals(this.home.toURI().toURL())) { + private List getNestedArchives(String path) throws Exception { + Archive parent = this.parent; + String root = path; + if (!root.equals("/") && root.startsWith("/") + || 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 +548,7 @@ public class PropertiesLauncher extends Launcher { @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 +637,9 @@ public class PropertiesLauncher extends Launcher { @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 3610844a85..98be63ec57 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.FileOutputStream; 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.junit.rules.TemporaryFolder; 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 class PropertiesLauncherTests { System.clearProperty("loader.config.name"); System.clearProperty("loader.config.location"); System.clearProperty("loader.system"); + System.clearProperty("loader.classLoader"); } @Test @@ -131,6 +136,16 @@ public class PropertiesLauncherTests { .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, endingWith("app.jar!/")); + } + @Test public void testUserSpecifiedWildcardPath() throws Exception { System.setProperty("loader.path", "jars/*"); @@ -153,13 +168,44 @@ public class PropertiesLauncherTests { 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, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("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, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("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, endingWith("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, endingWith("foo.jar!/")); + assertThat(archives).areExactly(1, endingWith("app.jar!/")); } @Test @@ -209,11 +255,28 @@ public class PropertiesLauncherTests { 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 archives = new ArrayList(); + String path = System.getProperty("java.class.path"); + for (String url : path.split(File.pathSeparator)) { + archives.add(archive(url)); + } + return archives; + } + + private Archive archive(String url) throws IOException { + File file = new FileSystemResource(url).getFile(); + if (url.endsWith(".jar")) { + return new JarFileArchive(file); + } + return new ExplodedArchive(file); + } + @Test public void testUserSpecifiedConfigPathWins() throws Exception { @@ -280,6 +343,17 @@ public class PropertiesLauncherTests { assertThat(timeout).as("Timed out waiting for (" + value + ")").isTrue(); } + private Condition endingWith(final String value) { + return new Condition() { + + @Override + public boolean matches(Archive archive) { + return archive.toString().endsWith(value); + } + + }; + } + public static class TestLoader extends URLClassLoader { public TestLoader(ClassLoader parent) { 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 a4365eec85..5600ed279e 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