Skip to content

Commit e4c807b

Browse files
Dave Syerwilkinsona
authored andcommitted
Tighten up PropertiesLauncher's contract
The main changes are: - Switch to `loader.properties` instead of `application.properties` - Search for `loader.properties` in `loader.home` as well as in the classpath - Placeholder replacements in MANIFEST.MF (using `loader.properties` or system/env vars) See gh-7221 Closes gh-8346
1 parent 5278bac commit e4c807b

File tree

8 files changed

+156
-64
lines changed

8 files changed

+156
-64
lines changed

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

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ files in directories (as opposed to explicitly on the classpath). In the case of
147147
you just add extra jars in those locations if you want more. The `PropertiesLauncher`
148148
looks in `BOOT-INF/lib/` in your application archive by default, but you can add
149149
additional locations by setting an environment variable `LOADER_PATH` or `loader.path`
150-
in `application.properties` (comma-separated list of directories or archives).
150+
in `loader.properties` (comma-separated list of directories or archives).
151151

152152

153153

@@ -198,7 +198,13 @@ the appropriate launcher:
198198

199199
`PropertiesLauncher` has a few special features that can be enabled with external
200200
properties (System properties, environment variables, manifest entries or
201-
`application.properties`).
201+
`loader.properties`).
202+
203+
NOTE: `PropertiesLauncher` supports loading properties from
204+
`loader.properties` and also (for historic reasons)
205+
`application.properties`. We recommend using
206+
`loader.properties` exclusively, as support for
207+
`application.properties` is deprecated and may be removed in the future.
202208

203209
|===
204210
|Key |Purpose
@@ -208,8 +214,7 @@ properties (System properties, environment variables, manifest entries or
208214
just like a regular `-classpath` on the `javac` command line.
209215

210216
|`loader.home`
211-
|Location of additional properties file, e.g. `file:///opt/app`
212-
(defaults to `${user.dir}`)
217+
|Used to resolve relative paths in `loader.path`. E.g. `loader.path=lib` then `${loader.home}/lib` is a classpath location (along with all jar files in that directory). Also used to locate a `loader.properties file`. Example `file:///opt/app` (defaults to `${user.dir}`).
213218

214219
|`loader.args`
215220
|Default arguments for the main method (space separated)
@@ -218,11 +223,11 @@ properties (System properties, environment variables, manifest entries or
218223
|Name of main class to launch, e.g. `com.app.Application`.
219224

220225
|`loader.config.name`
221-
|Name of properties file, e.g. `loader` (defaults to `application`).
226+
|Name of properties file, e.g. `launcher` (defaults to `loader`).
222227

223228
|`loader.config.location`
224229
|Path to properties file, e.g. `classpath:loader.properties` (defaults to
225-
`application.properties`).
230+
`loader.properties`).
226231

227232
|`loader.system`
228233
|Boolean flag to indicate that all properties should be added to System properties
@@ -241,7 +246,7 @@ be used:
241246
|`LOADER_PATH`
242247

243248
|`loader.home`
244-
|
249+
|`Loader-Home`
245250
|`LOADER_HOME`
246251

247252
|`loader.args`
@@ -253,11 +258,11 @@ be used:
253258
|`LOADER_MAIN`
254259

255260
|`loader.config.location`
256-
|
261+
|`Loader-Config-Location`
257262
|`LOADER_CONFIG_LOCATION`
258263

259264
|`loader.system`
260-
|
265+
|`Loader-System`
261266
|`LOADER_SYSTEM`
262267

263268
|===
@@ -266,15 +271,21 @@ TIP: Build plugins automatically move the `Main-Class` attribute to `Start-Class
266271
the fat jar is built. If you are using that, specify the name of the class to launch using
267272
the `Main-Class` attribute and leave out `Start-Class`.
268273

269-
* `loader.home` is the directory location of an additional properties file (overriding
274+
* `loader.properties` are searched for in `loader.home` then in the root of the classpath,
275+
then in `classpath:/BOOT-INF/classes`. The first location that exists is used.
276+
* `loader.home` is only the directory location of an additional properties file (overriding
270277
the default) as long as `loader.config.location` is not specified.
271278
* `loader.path` can contain directories (scanned recursively for jar and zip files),
272279
archive paths, or wildcard patterns (for the default JVM behavior).
273280
* `loader.path` (if empty) defaults to `BOOT-INF/lib` (meaning a local directory or a
274281
nested one if running from an archive). Because of this `PropertiesLauncher` behaves the
275282
same as `JarLauncher` when no additional configuration is provided.
283+
* `loader.path` can not be used to configure the location of `loader.properties` (the classpath
284+
used to search for the latter is the JVM classpath when `PropertiesLauncher` is launched).
276285
* Placeholder replacement is done from System and environment variables plus the
277286
properties file itself on all values before use.
287+
* The search order for properties (where it makes sense to look in more than one place)
288+
is env vars, system properties, `loader.properties`, exploded archive manifest, archive manifest.
278289

279290

280291

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

Lines changed: 93 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -142,48 +142,64 @@ public PropertiesLauncher() {
142142
}
143143

144144
protected File getHomeDirectory() {
145-
return new File(SystemPropertyUtils
146-
.resolvePlaceholders(System.getProperty(HOME, "${user.dir}")));
145+
try {
146+
return new File(getPropertyWithDefault(HOME, "${user.dir}"));
147+
}
148+
catch (Exception ex) {
149+
throw new IllegalStateException(ex);
150+
}
147151
}
148152

149153
private void initializeProperties() throws Exception, IOException {
150-
String config = "classpath:BOOT-INF/classes/"
151-
+ SystemPropertyUtils.resolvePlaceholders(
152-
SystemPropertyUtils.getProperty(CONFIG_NAME, "application"))
153-
+ ".properties";
154-
config = SystemPropertyUtils.resolvePlaceholders(
155-
SystemPropertyUtils.getProperty(CONFIG_LOCATION, config));
156-
InputStream resource = getResource(config);
157-
if (resource != null) {
158-
log("Found: " + config);
159-
try {
160-
this.properties.load(resource);
161-
}
162-
finally {
163-
resource.close();
154+
List<String> configs = new ArrayList<String>();
155+
if (getProperty(CONFIG_LOCATION) != null) {
156+
configs.add(getProperty(CONFIG_LOCATION));
157+
}
158+
else {
159+
String[] names = getPropertyWithDefault(CONFIG_NAME, "loader,application")
160+
.split(",");
161+
for (String name : names) {
162+
configs.add("file:" + getHomeDirectory() + "/" + name + ".properties");
163+
configs.add("classpath:" + name + ".properties");
164+
configs.add("classpath:BOOT-INF/classes/" + name + ".properties");
164165
}
165-
for (Object key : Collections.list(this.properties.propertyNames())) {
166-
String text = this.properties.getProperty((String) key);
167-
String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
168-
text);
169-
if (value != null) {
170-
this.properties.put(key, value);
166+
}
167+
for (String config : configs) {
168+
InputStream resource = getResource(config);
169+
if (resource != null) {
170+
log("Found: " + config);
171+
try {
172+
this.properties.load(resource);
173+
}
174+
finally {
175+
resource.close();
171176
}
172-
}
173-
if (SystemPropertyUtils
174-
.resolvePlaceholders("${" + SET_SYSTEM_PROPERTIES + ":false}")
175-
.equals("true")) {
176-
log("Adding resolved properties to System properties");
177177
for (Object key : Collections.list(this.properties.propertyNames())) {
178-
String value = this.properties.getProperty((String) key);
179-
System.setProperty((String) key, value);
178+
if (config.endsWith("application.properties")
179+
&& ((String) key).startsWith("loader.")) {
180+
warn("WARNING: use of application.properties for PropertiesLauncher is deprecated");
181+
}
182+
String text = this.properties.getProperty((String) key);
183+
String value = SystemPropertyUtils
184+
.resolvePlaceholders(this.properties, text);
185+
if (value != null) {
186+
this.properties.put(key, value);
187+
}
188+
}
189+
if ("true".equals(getProperty(SET_SYSTEM_PROPERTIES))) {
190+
log("Adding resolved properties to System properties");
191+
for (Object key : Collections.list(this.properties.propertyNames())) {
192+
String value = this.properties.getProperty((String) key);
193+
System.setProperty((String) key, value);
194+
}
180195
}
196+
// Load the first one we find
197+
return;
198+
}
199+
else {
200+
log("Not found: " + config);
181201
}
182202
}
183-
else {
184-
log("Not found: " + config);
185-
}
186-
187203
}
188204

189205
private InputStream getResource(String config) throws Exception {
@@ -354,34 +370,50 @@ private ClassLoader wrapWithCustomClassLoader(ClassLoader parent,
354370
}
355371

356372
private String getProperty(String propertyKey) throws Exception {
357-
return getProperty(propertyKey, null);
373+
return getProperty(propertyKey, null, null);
358374
}
359375

360376
private String getProperty(String propertyKey, String manifestKey) throws Exception {
377+
return getProperty(propertyKey, manifestKey, null);
378+
}
379+
380+
private String getPropertyWithDefault(String propertyKey, String defaultValue)
381+
throws Exception {
382+
return getProperty(propertyKey, null, defaultValue);
383+
}
384+
385+
private String getProperty(String propertyKey, String manifestKey,
386+
String defaultValue) throws Exception {
361387
if (manifestKey == null) {
362388
manifestKey = propertyKey.replace('.', '-');
363389
manifestKey = toCamelCase(manifestKey);
364390
}
365391
String property = SystemPropertyUtils.getProperty(propertyKey);
366392
if (property != null) {
367-
String value = SystemPropertyUtils.resolvePlaceholders(property);
393+
String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
394+
property);
368395
log("Property '" + propertyKey + "' from environment: " + value);
369396
return value;
370397
}
371398
if (this.properties.containsKey(propertyKey)) {
372-
String value = SystemPropertyUtils
373-
.resolvePlaceholders(this.properties.getProperty(propertyKey));
399+
String value = SystemPropertyUtils.resolvePlaceholders(this.properties,
400+
this.properties.getProperty(propertyKey));
374401
log("Property '" + propertyKey + "' from properties: " + value);
375402
return value;
376403
}
377404
try {
378-
// Prefer home dir for MANIFEST if there is one
379-
Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
380-
if (manifest != null) {
381-
String value = manifest.getMainAttributes().getValue(manifestKey);
382-
log("Property '" + manifestKey + "' from home directory manifest: "
383-
+ value);
384-
return value;
405+
if (this.home != null) {
406+
// Prefer home dir for MANIFEST if there is one
407+
Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
408+
if (manifest != null) {
409+
String value = manifest.getMainAttributes().getValue(manifestKey);
410+
if (value != null) {
411+
log("Property '" + manifestKey
412+
+ "' from home directory manifest: " + value);
413+
return SystemPropertyUtils.resolvePlaceholders(this.properties,
414+
value);
415+
}
416+
}
385417
}
386418
}
387419
catch (IllegalStateException ex) {
@@ -393,10 +425,11 @@ private String getProperty(String propertyKey, String manifestKey) throws Except
393425
String value = manifest.getMainAttributes().getValue(manifestKey);
394426
if (value != null) {
395427
log("Property '" + manifestKey + "' from archive manifest: " + value);
396-
return value;
428+
return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
397429
}
398430
}
399-
return null;
431+
return defaultValue == null ? defaultValue
432+
: SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue);
400433
}
401434

402435
@Override
@@ -436,10 +469,10 @@ private List<Archive> getClassPathArchives(String path) throws Exception {
436469
log("Adding classpath entries from archive " + archive.getUrl() + root);
437470
lib.add(archive);
438471
}
439-
Archive nested = getNestedArchive(root);
472+
List<Archive> nested = getNestedArchive(root);
440473
if (nested != null) {
441-
log("Adding classpath entries from nested " + nested.getUrl() + root);
442-
lib.add(nested);
474+
log("Adding classpath entries from nested " + root);
475+
lib.addAll(nested);
443476
}
444477
return lib;
445478
}
@@ -457,19 +490,21 @@ private Archive getArchive(File file) throws IOException {
457490
return null;
458491
}
459492

460-
private Archive getNestedArchive(String root) throws Exception {
493+
private List<Archive> getNestedArchive(String root) throws Exception {
494+
List<Archive> list = new ArrayList<Archive>();
461495
if (root.startsWith("/")
462496
|| this.parent.getUrl().equals(this.home.toURI().toURL())) {
463497
// If home dir is same as parent archive, no need to add it twice.
464-
return null;
498+
return list;
465499
}
466500
EntryFilter filter = new PrefixMatchingArchiveFilter(root);
467501
if (this.parent.getNestedArchives(filter).isEmpty()) {
468-
return null;
502+
return list;
469503
}
470504
// If there are more archives nested in this subdirectory (root) then create a new
471505
// virtual archive for them, and have it added to the classpath
472-
return new FilteredArchive(this.parent, filter);
506+
list.add(new FilteredArchive(this.parent, filter));
507+
return list;
473508
}
474509

475510
private void addNestedEntries(List<Archive> lib) {
@@ -548,6 +583,11 @@ private void log(String message) {
548583
}
549584
}
550585

586+
private void warn(String message) {
587+
// We shouldn't use java.util.logging because of classpath issues
588+
System.out.println(message);
589+
}
590+
551591
/**
552592
* Convenience class for finding nested archives that have a prefix in their file path
553593
* (e.g. "lib/").

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.junit.Before;
3232
import org.junit.Rule;
3333
import org.junit.Test;
34+
import org.junit.rules.ExpectedException;
3435
import org.junit.rules.TemporaryFolder;
3536
import org.mockito.MockitoAnnotations;
3637

@@ -50,6 +51,9 @@ public class PropertiesLauncherTests {
5051
@Rule
5152
public InternalOutputCapture output = new InternalOutputCapture();
5253

54+
@Rule
55+
public ExpectedException expected = ExpectedException.none();
56+
5357
@Rule
5458
public TemporaryFolder temporaryFolder = new TemporaryFolder();
5559

@@ -72,9 +76,28 @@ public void close() {
7276

7377
@Test
7478
public void testDefaultHome() {
79+
System.clearProperty("loader.home");
80+
PropertiesLauncher launcher = new PropertiesLauncher();
81+
assertThat(launcher.getHomeDirectory())
82+
.isEqualTo(new File(System.getProperty("user.dir")));
83+
}
84+
85+
@Test
86+
public void testAlternateHome() throws Exception {
87+
System.setProperty("loader.home", "src/test/resources/home");
7588
PropertiesLauncher launcher = new PropertiesLauncher();
7689
assertThat(launcher.getHomeDirectory())
7790
.isEqualTo(new File(System.getProperty("loader.home")));
91+
assertThat(launcher.getMainClass()).isEqualTo("demo.HomeApplication");
92+
}
93+
94+
@Test
95+
public void testNonExistentHome() throws Exception {
96+
System.setProperty("loader.home", "src/test/resources/nonexistent");
97+
this.expected.expectMessage("Invalid source folder");
98+
PropertiesLauncher launcher = new PropertiesLauncher();
99+
assertThat(launcher.getHomeDirectory())
100+
.isNotEqualTo(new File(System.getProperty("loader.home")));
78101
}
79102

80103
@Test
@@ -93,6 +116,13 @@ public void testUserSpecifiedConfigName() throws Exception {
93116
.isEqualTo("[etc/]");
94117
}
95118

119+
@Test
120+
public void testRootOfClasspathFirst() throws Exception {
121+
System.setProperty("loader.config.name", "bar");
122+
PropertiesLauncher launcher = new PropertiesLauncher();
123+
assertThat(launcher.getMainClass()).isEqualTo("my.BarApplication");
124+
}
125+
96126
@Test
97127
public void testUserSpecifiedDotPath() throws Exception {
98128
System.setProperty("loader.path", ".");
@@ -232,6 +262,13 @@ public void testLoadPathCustomizedUsingManifest() throws Exception {
232262
.containsExactly("/foo.jar", "/bar/");
233263
}
234264

265+
@Test
266+
public void testManifestWithPlaceholders() throws Exception {
267+
System.setProperty("loader.home", "src/test/resources/placeholders");
268+
PropertiesLauncher launcher = new PropertiesLauncher();
269+
assertThat(launcher.getMainClass()).isEqualTo("demo.FooApplication");
270+
}
271+
235272
private void waitFor(String value) throws Exception {
236273
int count = 0;
237274
boolean timeout = false;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
loader.main: demo.Application
1+
loader.main: demo.NoSuchApplication
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
loader.main: my.BootInfBarApplication

0 commit comments

Comments
 (0)