Skip to content

Commit 73a4205

Browse files
committed
Add jarmode support to the loader code
Update the `Launcher` class to allow a packaged jar to be launched in a different mode. The launcher now checks for a `jarmode` property and attempts to find a `JarMode` implementation using the standard `spring.factories` mechanism. Closes gh-19848
1 parent d5a7068 commit 73a4205

File tree

9 files changed

+291
-8
lines changed

9 files changed

+291
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader;
18+
19+
import java.net.URL;
20+
import java.net.URLClassLoader;
21+
22+
/**
23+
* {@link URLClassLoader} used for exploded archives.
24+
*
25+
* @author Phillip Webb
26+
*/
27+
class ExplodedURLClassLoader extends URLClassLoader {
28+
29+
ExplodedURLClassLoader(URL[] urls, ClassLoader parent) {
30+
super(urls, parent);
31+
}
32+
33+
@Override
34+
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
35+
try {
36+
Class<?> result = findClass(name);
37+
if (resolve) {
38+
resolveClass(result);
39+
}
40+
return result;
41+
}
42+
catch (ClassNotFoundException ex) {
43+
}
44+
return super.loadClass(name, resolve);
45+
}
46+
47+
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -75,6 +75,17 @@ public Enumeration<URL> findResources(String name) throws IOException {
7575

7676
@Override
7777
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
78+
if (name.startsWith("org.springframework.boot.loader.jarmode.")) {
79+
try {
80+
Class<?> result = findClass(name);
81+
if (resolve) {
82+
resolveClass(result);
83+
}
84+
return result;
85+
}
86+
catch (ClassNotFoundException ex) {
87+
}
88+
}
7889
Handler.setUseFastConnectionExceptions(true);
7990
try {
8091
try {

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.io.File;
2020
import java.net.URI;
2121
import java.net.URL;
22-
import java.net.URLClassLoader;
2322
import java.security.CodeSource;
2423
import java.security.ProtectionDomain;
2524
import java.util.ArrayList;
@@ -41,6 +40,8 @@
4140
*/
4241
public abstract class Launcher {
4342

43+
private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher";
44+
4445
/**
4546
* Launch the application. This method is the initial entry point that should be
4647
* called by a subclass {@code public static void main(String[] args)} method.
@@ -52,7 +53,9 @@ protected void launch(String[] args) throws Exception {
5253
JarFile.registerUrlProtocolHandler();
5354
}
5455
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
55-
launch(args, getMainClass(), classLoader);
56+
String jarMode = System.getProperty("jarmode");
57+
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
58+
launch(args, launchClass, classLoader);
5659
}
5760

5861
/**
@@ -94,19 +97,19 @@ protected ClassLoader createClassLoader(URL[] urls) throws Exception {
9497
if (supportsNestedJars()) {
9598
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
9699
}
97-
return new URLClassLoader(urls, getClass().getClassLoader());
100+
return new ExplodedURLClassLoader(urls, getClass().getClassLoader());
98101
}
99102

100103
/**
101104
* Launch the application given the archive file and a fully configured classloader.
102105
* @param args the incoming arguments
103-
* @param mainClass the main class to run
106+
* @param launchClass the launch class to run
104107
* @param classLoader the classloader
105108
* @throws Exception if the launch fails
106109
*/
107-
protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
110+
protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception {
108111
Thread.currentThread().setContextClassLoader(classLoader);
109-
createMainMethodRunner(mainClass, args, classLoader).run();
112+
createMainMethodRunner(launchClass, args, classLoader).run();
110113
}
111114

112115
/**

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -45,6 +45,7 @@ public MainMethodRunner(String mainClass, String[] args) {
4545
public void run() throws Exception {
4646
Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
4747
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
48+
mainMethod.setAccessible(true);
4849
mainMethod.invoke(null, new Object[] { this.args });
4950
}
5051

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.jarmode;
18+
19+
/**
20+
* Interface registered in {@code spring.factories} to provides extended 'jarmode'
21+
* support.
22+
*
23+
* @author Phillip Webb
24+
* @since 2.3.0
25+
*/
26+
public interface JarMode {
27+
28+
/**
29+
* Returns if this accepts and can run the given mode.
30+
* @param mode the mode to check
31+
* @return if this instance accepts the mode
32+
*/
33+
boolean accepts(String mode);
34+
35+
/**
36+
* Run the jar in the given mode.
37+
* @param mode the mode to use
38+
* @param args any program arguments
39+
*/
40+
void run(String mode, String[] args);
41+
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.jarmode;
18+
19+
import java.util.List;
20+
21+
import org.springframework.core.io.support.SpringFactoriesLoader;
22+
import org.springframework.util.ClassUtils;
23+
24+
/**
25+
* Delegate class used to launch the fat jar in a specific mode.
26+
*
27+
* @author Phillip Webb
28+
* @since 2.3.0
29+
*/
30+
public final class JarModeLauncher {
31+
32+
static final String DISABLE_SYSTEM_EXIT = JarModeLauncher.class.getName() + ".DISABLE_SYSTEM_EXIT";
33+
34+
private JarModeLauncher() {
35+
}
36+
37+
public static void main(String[] args) {
38+
String mode = System.getProperty("jarmode");
39+
List<JarMode> candidates = SpringFactoriesLoader.loadFactories(JarMode.class,
40+
ClassUtils.getDefaultClassLoader());
41+
for (JarMode candidate : candidates) {
42+
if (candidate.accepts(mode)) {
43+
candidate.run(mode, args);
44+
return;
45+
}
46+
}
47+
System.err.println("Unsupported jarmode '" + mode + "'");
48+
if (!Boolean.getBoolean(DISABLE_SYSTEM_EXIT)) {
49+
System.exit(1);
50+
}
51+
}
52+
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.jarmode;
18+
19+
import java.util.Arrays;
20+
21+
/**
22+
* {@link JarMode} for testing.
23+
*
24+
* @author Phillip Webb
25+
*/
26+
class TestJarMode implements JarMode {
27+
28+
@Override
29+
public boolean accepts(String mode) {
30+
return "test".equals(mode);
31+
}
32+
33+
@Override
34+
public void run(String mode, String[] args) {
35+
System.out.println("running in " + mode + " jar mode " + Arrays.asList(args));
36+
}
37+
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.loader.jarmode;
18+
19+
import java.util.Collections;
20+
import java.util.Iterator;
21+
22+
import org.junit.jupiter.api.AfterEach;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
27+
import org.springframework.boot.loader.Launcher;
28+
import org.springframework.boot.loader.archive.Archive;
29+
import org.springframework.boot.testsupport.system.CapturedOutput;
30+
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
31+
32+
import static org.assertj.core.api.Assertions.assertThat;
33+
34+
/**
35+
* Tests for {@link Launcher} with jar mode support.
36+
*
37+
* @author Phillip Webb
38+
*/
39+
@ExtendWith(OutputCaptureExtension.class)
40+
class LauncherJarModeTests {
41+
42+
@BeforeEach
43+
void setup() {
44+
System.setProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT, "true");
45+
}
46+
47+
@AfterEach
48+
void cleanup() {
49+
System.clearProperty("jarmode");
50+
System.clearProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT);
51+
}
52+
53+
@Test
54+
void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception {
55+
System.setProperty("jarmode", "test");
56+
new TestLauncher().launch(new String[] { "boot" });
57+
assertThat(out).contains("running in test jar mode [boot]");
58+
}
59+
60+
@Test
61+
void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception {
62+
System.setProperty("jarmode", "idontexist");
63+
new TestLauncher().launch(new String[] { "boot" });
64+
assertThat(out).contains("Unsupported jarmode 'idontexist'");
65+
}
66+
67+
private static class TestLauncher extends Launcher {
68+
69+
@Override
70+
protected String getMainClass() throws Exception {
71+
throw new IllegalStateException("Should not be called");
72+
}
73+
74+
@Override
75+
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
76+
return Collections.emptyIterator();
77+
}
78+
79+
@Override
80+
protected void launch(String[] args) throws Exception {
81+
super.launch(args);
82+
}
83+
84+
}
85+
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.boot.loader.jarmode.JarMode=\
2+
org.springframework.boot.loader.jarmode.TestJarMode

0 commit comments

Comments
 (0)