From 6687e2460b5cd39bada613f8745bfabf0449879e Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 3 Mar 2017 16:44:43 +0000 Subject: [PATCH] All PropertiesLauncher to locate classes as well as nested jars Before this change application classes only work in BOOT-INF/classes. With this change 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 --- .../appendix-executable-jar-format.adoc | 1 + .../boot/loader/PropertiesLauncher.java | 73 +++++++++--- .../boot/loader/PropertiesLauncherTests.java | 108 +++++++++++++++++- .../src/test/resources/nested-jars/app.jar | Bin 3081 -> 3313 bytes 4 files changed, 159 insertions(+), 23 deletions(-) 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 a4365eec85b5b8c296c9a6466973a15d818e96b2..5600ed279efb99189791ed00b3bbb4203efaa208 100644 GIT binary patch literal 3313 zcmc&$&u`pB6n@^_WV6nuNq;15Xxh2~q)k!0O}QYpMNPK}O_Q`y6M{s6%GsR+x4Rz8 z_NFZmp^5|Q2_a4hlmkM5I3U3R5E5MA)b@n<11fPqNN_+1aX}T|vmM*JF%=R*cr~_n z{NBv_@qO>j!fZCD67>*`oW6OM5?6t8G(WX8IX-i4x_s{z(L$T(z|Qwary$N_4@M+9 zKY4CudTMb=pP!yLEOS-3-uSX`TvJ+)YZLm>KDFuy!xQ!KQ`dRoG5zSJiDMI5>AYjq z8bYf|$Ci%aL4K@H?Nj4z4`1CpoiGsg5gE2!jwK$1#LuH2epJd3@#qUBrlr(t4a0SB z-TU%u%sonnRqCc)1=>w}dT3CkA=;a#eR&$r(*p{1A2%)2dqSaXX>3`cTvgVELj7~5 zCC;^)E5cbaRvMVeIxS0~q0+^<%f`AY{8pH8E|nY!4g3VhRmk zXjz`w6w9V-Vx39LlAiCdtI&}->8zDqn-^JijHb9MohxP66ONGHbz79HvS~|8SYD-w zv=#ESzepo=K%pJjK!ZURG<99i)4?JgB2A$@2MHe)8i>t2zj9gBJnWaY-HMbRZ`3rb zIy((}E#b83FJ6aDO@u%=_%Ij*z?9_$815kk!e~|$+U;-HJ`ID|JFkJz4VKa(M~A5k z>46|}mBAB!;trhB0X!8vyN-NL$|rbHs26t@#w6XKB9b16J`7c0fC`^dW>@wz%EjWU z|DnfUH$yuhl%aka2p8I!#5#lSi=Q?x3}XWxMF@rQ^s{LjwJXM&&@fj~O{B*0G=x;| z$e_+4)whN8zAACjSusqydNO)f?bb@etZ6>7HO@ZvZ-^$a;etjT+0$N93n$O7BVA@) zsD)5>U7B?UO(}paaHAQ{nmYz*EpY|e)}gG2xhsvf!)WM8>zNP8PLWrCy@zlf)V><3mpDW*vIK&?!hVC3;379J*H&9y$nQp!47^BG53(h zPVTXIz`Yz+?egn$4uU%{`tdD812HCU0~1ev@N463eBd#N(HXrq3i8KFJ1G)@4iO#V z-sOT@(|q_d3PSu}d@h8)_kts@x>^+d1Rp{!Ao4L661mHEN!8OyT}*w=!CutVwItTx&T( zpR)MOGl{usIOkX86VIxKZR^ugqTqMi@x+a4uW$&@%{p*nb$!8 literal 3081 zcmai$4LsBP9>@Q)&8AU#wkRb>9yj5JvRGKQ(HKeR-rNma30cFb$kXKr-8!dMhzLo~ zz3Ab@={nh*I>Mnj9y&R);=ClBhRQuB)&2dq@V~ax`S11H{{PqO|9kKAefWL9-#{iB zvj~DO>wRTDikpvLT96qO;2T19pl_j(w}xmzXb%)O*zfsFXwrTHgu=W=K@i3Oyc`G+ z@GoA0Fj^Mo>kXb@mH`qGK;1&8`38rO0%&tIn7Dv870}k|&vYP3b(jve?QI=5*p6M! zvR{QHMu0s%K$>(3qg6CY{Jx~fcGrK6KeHOHvUM9aHXJhktN=P0Gbx-9q7Z@_;>vu& z`Jz@>3>_>YQc!|G7`Lz7a_vy$g zh9W49n#I~Yy+>Bw`Rg{69zFac^!G=6!GYu1E>Ra|e)c#*d-nMF6PJ3wm=eOhfAkL( zTHR$7rN7PEJZTV&kvuGaubos z*y%-q`wRUIJ1^V~ll1v#PZnn9{>yCJR~ugW`_-~vOYJ;wi^j`K{P6;QPGan)LK-Tq zQg-qBAK#YejtB9~@6?P_`L@M%hhnaZo0qoS)cI%kl@a23{hqg?y_FUNXi=4GYwL@p zC67-eqhOnrncj7<3@!$97Z%tY#fytqrh0}*%>HCSX#?BW4yHn$z+E!1jApk}1%lY< zNN!?u{2oNk3jj}D&Ij&bYETU*XD^=}EvSw@Q%%A2coQ0yQGKLYA)GF6*5LZ)=CStc z*MdARuUM4)9QW#MOElld)xv|9;O7P=9 z;aEAfPU7syBa`2qqLq0B=hX?#ay~y`Ve@j05NC!xnuIfb6lGn?yc+Pf$0xa2nppFt z(Z!rj*Wb)-s5fqtagihUv~Lk#ZIGDNesIe>$08KlNJj?-?FPf8V{7A9emU?i+Ifky z-0^|S->&R?-)Gv_EbX;ggD$Z5{l;&*$Yb@Hz7%XqWky_)7!kByj4sSzXXhLA_>1ow?j&d)(vLziZ5Aa4QJM=JOM=X&{~@U>T|l$6PA!0nvchf>aJT zOj#^()TT|Jmb~a9HF&Mjw0yHzJ9m|3P_7`t?h69`^>3K(8~eUH^FvR6thUU%`hi?1 z9%`&G%v_#82-P>-Puw-bmxitnvpLYb?qpg|F~g%&++&S?eyei_=Yt^g!`PCkV3(m| zM$7Bn^Uu9pj5;RsGfZ{$9~kAYi+nO~3_UR2@06xx6dM?F8Y~{ z^P`SO-aofPqdmM6Zz0a9-K5>*n0r#EDq9y%$-L#e>QC>aiC5>Rhe+vnZy0x?D5xR)v*Cd z?n07PhX{aIGe98ypxXYCeux43mBRoasc8jB7pRT`vn1FB7eA5^g8@LogMr!{P