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 da73c4dd7a5..05226419ddb 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 @@ -142,7 +142,7 @@ Their purpose is to load resources (`.class` files etc.) from nested jar files o files in directories (as opposed to explicitly on the classpath). In the case of the `[Jar|War]Launcher` the nested paths are fixed (`+lib/*.jar+` and `+lib-provided/*.jar+` for the war case) so you just add extra jars in those locations if you want more. The -`PropertiesLauncher` looks in `lib/` by default, but you can add additional locations by +`PropertiesLauncher` looks in `lib/` in your application archive by default, but you can add additional locations by setting an environment variable `LOADER_PATH` or `loader.path` in `application.properties` (comma-separated list of directories or archives). diff --git a/spring-boot-tools/spring-boot-loader/pom.xml b/spring-boot-tools/spring-boot-loader/pom.xml index 8576f39155c..0497615b45a 100644 --- a/spring-boot-tools/spring-boot-loader/pom.xml +++ b/spring-boot-tools/spring-boot-loader/pom.xml @@ -30,6 +30,11 @@ logback-classic test + + org.springframework + spring-webmvc + test + org.bouncycastle @@ -57,6 +62,7 @@ true ${skipTests} true + 4 diff --git a/spring-boot-tools/spring-boot-loader/src/it/executable-props/pom.xml b/spring-boot-tools/spring-boot-loader/src/it/executable-props/pom.xml new file mode 100644 index 00000000000..089833d2f56 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/it/executable-props/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + org.springframework.boot.launcher.it + executable-props + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.1 + + 1.6 + 1.6 + + + + org.apache.maven.plugins + maven-dependency-plugin + 2.9 + + + unpack + prepare-package + + unpack + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + jar + + + ${project.build.directory}/assembly + + + + copy + prepare-package + + copy-dependencies + + + ${project.build.directory}/assembly/lib + + + + + + maven-assembly-plugin + 2.4 + + + src/main/assembly/jar-with-dependencies.xml + + + + org.springframework.boot.loader.PropertiesLauncher + + + org.springframework.boot.load.it.props.EmbeddedJarStarter + + + + + + jar-with-dependencies + package + + single + + + + + + + + + org.springframework + spring-context + 4.1.4.RELEASE + + + diff --git a/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/assembly/jar-with-dependencies.xml b/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/assembly/jar-with-dependencies.xml new file mode 100644 index 00000000000..44626f91aa1 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/assembly/jar-with-dependencies.xml @@ -0,0 +1,26 @@ + + + full + + jar + + false + + + + + ${project.groupId}:${project.artifactId} + + true + + + + + ${project.build.directory}/assembly + / + + + diff --git a/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/EmbeddedJarStarter.java b/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/EmbeddedJarStarter.java new file mode 100644 index 00000000000..12936e2c706 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/EmbeddedJarStarter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.load.it.props; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +/** + * Main class to start the embedded server. + * + * @author Phillip Webb + */ +public final class EmbeddedJarStarter { + + public static void main(String[] args) throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfiguration.class); + context.getBean(SpringConfiguration.class).run(args); + context.close(); + } +} diff --git a/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/SpringConfiguration.java b/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/SpringConfiguration.java new file mode 100644 index 00000000000..54e39662f25 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/java/org/springframework/launcher/it/props/SpringConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.load.it.props; + +import java.io.IOException; + +import javax.annotation.PostConstruct; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.support.PropertiesLoaderUtils; + +/** + * Spring configuration. + * + * @author Phillip Webb + */ +@Configuration +@ComponentScan +public class SpringConfiguration { + + private String message = "Jar"; + + @PostConstruct + public void init() throws IOException { + String value = PropertiesLoaderUtils.loadAllProperties("application.properties").getProperty("message"); + if (value!=null) { + this.message = value; + } + + } + + public void run(String... args) { + System.err.println("Hello Embedded " + this.message + "!"); + } + + + +} diff --git a/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/resources/application.properties b/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/resources/application.properties new file mode 100644 index 00000000000..c11051e3477 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/it/executable-props/src/main/resources/application.properties @@ -0,0 +1 @@ +message: World \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-loader/src/it/executable-props/verify.groovy b/spring-boot-tools/spring-boot-loader/src/it/executable-props/verify.groovy new file mode 100644 index 00000000000..80892f628e8 --- /dev/null +++ b/spring-boot-tools/spring-boot-loader/src/it/executable-props/verify.groovy @@ -0,0 +1,32 @@ +def jarfile = './target/executable-props-0.0.1.BUILD-SNAPSHOT-full.jar' + +new File("${basedir}/application.properties").delete() + +String exec(String command) { + def proc = command.execute([], basedir) + proc.waitFor() + proc.err.text +} + +String out = exec("java -jar ${jarfile}") +assert out.contains('Hello Embedded World!'), + 'Using -jar my.jar should use the application.properties from the jar\n' + out + +out = exec("java -cp ${jarfile} org.springframework.boot.loader.PropertiesLauncher") +assert out.contains('Hello Embedded World!'), + 'Using -cp my.jar with PropertiesLauncher should use the application.properties from the jar\n' + out + +new File("${basedir}/application.properties").withWriter { it -> it << "message: Foo" } +out = exec("java -jar ${jarfile}") +assert out.contains('Hello Embedded World!'), + 'Should use the application.properties from the jar in preference to local filesystem\n' + out + +out = exec("java -Dloader.path=.,lib -jar ${jarfile}") +assert out.contains('Hello Embedded Foo!'), + 'With loader.path=.,lib should use the application.properties from the local filesystem\n' + out + +new File("${basedir}/target/application.properties").withWriter { it -> it << "message: Spam" } + +out = exec("java -Dloader.path=target,.,lib -jar ${jarfile}") +assert out.contains('Hello Embedded Spam!'), + 'With loader.path=target,.,lib should use the application.properties from the target directory\n' + out \ No newline at end of file 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 6f8d6eeeb71..2cd66f0073d 100644 --- 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 @@ -124,7 +124,7 @@ public class PropertiesLauncher extends Launcher { */ public static final String SET_SYSTEM_PROPERTIES = "loader.system"; - private static final List DEFAULT_PATHS = Arrays.asList("lib/"); + private static final List DEFAULT_PATHS = Arrays.asList(); private static final Pattern WORD_SEPARATOR = Pattern.compile("\\W+"); @@ -136,6 +136,8 @@ public class PropertiesLauncher extends Launcher { private final Properties properties = new Properties(); + private Archive parent; + public PropertiesLauncher() { if (!isDebug()) { this.logger.setLevel(Level.SEVERE); @@ -144,6 +146,7 @@ public class PropertiesLauncher extends Launcher { this.home = getHomeDirectory(); initializeProperties(this.home); initializePaths(); + this.parent = createArchive(); } catch (Exception ex) { throw new IllegalStateException(ex); @@ -314,15 +317,12 @@ public class PropertiesLauncher extends Launcher { 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(".") || path.equals(""))) { + if (!path.equals("")) { paths.add(path); } } if (paths.isEmpty()) { - // On the other hand, we don't want a completely empty path. If the app is - // running from an archive (java -jar) then this will make sure the archive - // itself is included at the very least. - paths.add("."); + paths.add("lib"); } return paths; } @@ -449,6 +449,8 @@ public class PropertiesLauncher extends Launcher { } } addParentClassLoaderEntries(lib); + // Entries are reversed when added to the actual classpath + Collections.reverse(lib); return lib; } @@ -493,41 +495,84 @@ public class PropertiesLauncher extends Launcher { } private Archive getNestedArchive(final String root) throws Exception { - Archive parent = createArchive(); - if (root.startsWith("/") || parent.getUrl().equals(this.home.toURI().toURL())) { + if (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; } EntryFilter filter = new PrefixMatchingArchiveFilter(root); - if (parent.getNestedArchives(filter).isEmpty()) { + if (this.parent.getNestedArchives(filter).isEmpty()) { return null; } // If there are more archives nested in this subdirectory (root) then create a new // virtual archive for them, and have it added to the classpath - return new FilteredArchive(parent, filter); + return new FilteredArchive(this.parent, filter); } private void addParentClassLoaderEntries(List lib) throws IOException, URISyntaxException { ClassLoader parentClassLoader = getClass().getClassLoader(); + List urls = new ArrayList(); for (URL url : getURLs(parentClassLoader)) { if (url.toString().endsWith(".jar") || url.toString().endsWith(".zip")) { - lib.add(0, new JarFileArchive(new File(url.toURI()))); + urls.add(new JarFileArchive(new File(url.toURI()))); } else if (url.toString().endsWith("/*")) { String name = url.getFile(); File dir = new File(name.substring(0, name.length() - 1)); if (dir.exists()) { - lib.add(0, - new ExplodedArchive(new File(name.substring(0, - name.length() - 1)), false)); + urls.add(new ExplodedArchive(new File(name.substring(0, + name.length() - 1)), false)); } } else { String filename = URLDecoder.decode(url.getFile(), "UTF-8"); - lib.add(0, new ExplodedArchive(new File(filename))); + urls.add(new ExplodedArchive(new File(filename))); } } + // The parent archive might have a "lib/" directory, meaning we are running from + // an executable JAR. We add nested entries from there with low priority (i.e. at + // end). + addNestedArchivesFromParent(urls); + for (Archive archive : urls) { + // But only add them if they are not already included + if (findArchive(lib, archive) < 0) { + lib.add(archive); + } + } + } + + private void addNestedArchivesFromParent(List urls) { + int index = findArchive(urls, this.parent); + if (index >= 0) { + try { + Archive nested = getNestedArchive("lib/"); + if (nested != null) { + List extra = new ArrayList( + nested.getNestedArchives(new ArchiveEntryFilter())); + urls.addAll(index + 1, extra); + } + } + catch (Exception e) { + // ignore + } + } + } + + private int findArchive(List urls, Archive archive) { + // Do not rely on Archive to have an equals() method. Look for the archive by + // matching strings. + if (archive == null) { + return -1; + } + int i = 0; + for (Archive url : urls) { + if (url.toString().equals(archive.toString())) { + return i; + } + i++; + } + return -1; } private URL[] getURLs(ClassLoader classLoader) {