Skip to content

Commit 481aaeb

Browse files
Kehrlannrwinch
authored andcommitted
Add JarLauncherDetector main class
- When executing a Spring Boot fat jar, detects the correct JarLauncher to use based on what is available.
1 parent 7f3c940 commit 481aaeb

File tree

7 files changed

+241
-8
lines changed

7 files changed

+241
-8
lines changed

config/checkstyle/checkstyle-suppressions.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66
<!-- Ignore third-party code -->
77
<suppress files="LoggingMavenRepositoryListener\.java|LoggingMavenTransferListener\.java" checks=".*"/>
88
<suppress files="SpringBootApplicationMain\.java" checks=".*"/>
9+
<suppress files="JarLauncherDetector\.java" checks=".*"/>
910
</suppressions>

spring-boot-testjars/src/main/java/org/springframework/experimental/boot/server/exec/CommonsExecWebServerFactoryBean.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 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.
@@ -37,6 +37,7 @@
3737
* application.properties respectively.
3838
*
3939
* @author Rob Winch
40+
* @author Daniel Garnier-Moiroux
4041
*/
4142
public class CommonsExecWebServerFactoryBean
4243
implements FactoryBean<CommonsExecWebServer>, DisposableBean, BeanNameAware {
@@ -49,16 +50,17 @@ public class CommonsExecWebServerFactoryBean
4950

5051
private Map<String, String> systemProperties = new HashMap<>();
5152

52-
private String mainClass = "org.springframework.boot.loader.JarLauncher";
53+
private String mainClass = "org.springframework.experimental.boot.server.exec.detector.JarLauncherDetector";
5354

5455
private File applicationPortFile = createApplicationPortFile();
5556

5657
private CommonsExecWebServer webServer;
5758

5859
CommonsExecWebServerFactoryBean() {
60+
Class<?> jarDetector = ClassUtils.resolveClassName(this.mainClass, null);
5961
this.classpath.entries(new ResourceClasspathEntry(
6062
"org/springframework/experimental/boot/testjars/classpath-entries/META-INF/spring.factories",
61-
"META-INF/spring.factories"));
63+
"META-INF/spring.factories"), new RecursiveResourceClasspathEntry(jarDetector));
6264
}
6365

6466
public static CommonsExecWebServerFactoryBean builder() {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2012-2024 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.experimental.boot.server.exec.detector;
18+
19+
import java.lang.reflect.InvocationTargetException;
20+
21+
import org.springframework.experimental.boot.server.exec.main.SpringBootApplicationMain;
22+
23+
/**
24+
* Detect which JarLauncher main class to use, and call its {@code main(String[] args)}
25+
* methods.
26+
* <p>
27+
* The location depends on the Boot version. Prior to 3.2, it was in the
28+
* {@code org.springframework.boot.loader} package. In 3.2, it was moved to the
29+
* {@code org.springframework.boot.loader.launch} package.
30+
*
31+
* @author Daniel Garnier-Moiroux
32+
*/
33+
public class JarLauncherDetector {
34+
35+
public static void main(String[] args) {
36+
try {
37+
// Boot >= 3.2
38+
Class<?> jarLauncher = loadClass("org.springframework.boot.loader.launch.JarLauncher");
39+
var mainMethod = jarLauncher.getMethod("main", String[].class);
40+
mainMethod.invoke(null, (Object) args);
41+
return;
42+
}
43+
catch (ClassNotFoundException ignored) {
44+
// no-op
45+
}
46+
catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
47+
// TODO: log?
48+
throw new RuntimeException(ex);
49+
}
50+
51+
try {
52+
// Boot < 3.2
53+
Class<?> jarLauncher = loadClass("org.springframework.boot.loader.JarLauncher");
54+
var mainMethod = jarLauncher.getMethod("main", String[].class);
55+
mainMethod.invoke(null, (Object) args);
56+
return;
57+
}
58+
catch (ClassNotFoundException ignored) {
59+
// no-op
60+
}
61+
catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException ex) {
62+
// TODO: log?
63+
throw new RuntimeException(ex);
64+
}
65+
66+
SpringBootApplicationMain.main(args);
67+
}
68+
69+
// Helpful for testing, because Class.forName cannot be mocked
70+
public static Class<?> loadClass(String className) throws ClassNotFoundException {
71+
return Class.forName(className);
72+
}
73+
74+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-2024 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+
/**
18+
* Provides a default main class to run detect the correct Spring Boot JarLauncher.
19+
*/
20+
package org.springframework.experimental.boot.server.exec.detector;

spring-boot-testjars/src/main/java/org/springframework/experimental/boot/server/exec/main/package-info.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
*/
1616

1717
/**
18-
* Provides a default main class to run Spring Boot applications against. This is
19-
* intentionally left otherwise empty to avoid scanning classes unnecessarily.
18+
* Provides a main class which detects which Spring Boot JarLauncher to use. It also
19+
* provides a default class to run Spring Boot applications against. This is intentionally
20+
* left otherwise empty to avoid scanning classes unnecessarily.
2021
*/
2122
package org.springframework.experimental.boot.server.exec.main;

spring-boot-testjars/src/test/java/org/springframework/experimental/boot/server/exec/CommonsExecWebServerFactoryBeanTests.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 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.
@@ -17,8 +17,10 @@
1717
package org.springframework.experimental.boot.server.exec;
1818

1919
import java.io.File;
20+
import java.net.MalformedURLException;
2021
import java.net.URL;
2122
import java.net.URLClassLoader;
23+
import java.util.ArrayList;
2224
import java.util.Arrays;
2325
import java.util.List;
2426

@@ -37,8 +39,8 @@ void classpathContainsSpringFactories() throws Exception {
3739
int index = args.indexOf("-classpath");
3840
assertThat(index).isGreaterThanOrEqualTo(0);
3941
assertThat(args).hasSizeGreaterThan(index + 1);
40-
String classpath = args.get(index + 1);
41-
URLClassLoader loader = new URLClassLoader(new URL[] { new File(classpath).toURI().toURL() }, null);
42+
String classpathArgs = args.get(index + 1);
43+
var loader = getClassLoaderFromArgs(classpathArgs);
4244
assertThat(loader.findResource("META-INF/spring.factories")).isNotNull();
4345
server.destroy();
4446
}
@@ -75,4 +77,38 @@ void mainClassWhenNull() {
7577
.isThrownBy(() -> CommonsExecWebServerFactoryBean.builder().mainClass(mainClass));
7678
}
7779

80+
@Test
81+
void usesJarLauncherwhenNoMainClassDefined() throws Exception {
82+
CommonsExecWebServer webServer = CommonsExecWebServerFactoryBean.builder().getObject();
83+
String[] args = webServer.getCommandLine().getArguments();
84+
assertThat(args[args.length - 1])
85+
.isEqualTo("org.springframework.experimental.boot.server.exec.detector.JarLauncherDetector");
86+
}
87+
88+
@Test
89+
void doesNotAddJarLauncherDetectorLauncherDetectorWhenMainClassDefined() throws Exception {
90+
String mainClass = "example.Main";
91+
CommonsExecWebServer server = CommonsExecWebServerFactoryBean.builder().mainClass(mainClass).getObject();
92+
List<String> args = Arrays.asList(server.getCommandLine().getArguments());
93+
int index = args.indexOf("-classpath");
94+
assertThat(index).isGreaterThanOrEqualTo(0);
95+
assertThat(args).hasSizeGreaterThan(index + 1);
96+
String classpathArgs = args.get(index + 1);
97+
URLClassLoader loader = getClassLoaderFromArgs(classpathArgs);
98+
assertThat(
99+
loader.findResource("org.springframework.experimental.boot.server.exec.detector.JarLauncherDetector"))
100+
.isNull();
101+
server.destroy();
102+
}
103+
104+
private static URLClassLoader getClassLoaderFromArgs(String classpathArgs) throws MalformedURLException {
105+
var paths = new ArrayList<URL>();
106+
for (String path : classpathArgs.split(":")) {
107+
var url = new File(path).toURI().toURL();
108+
paths.add(url);
109+
}
110+
URLClassLoader loader = new URLClassLoader(paths.toArray(new URL[] {}), null);
111+
return loader;
112+
}
113+
78114
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2012-2024 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.experimental.boot.server.exec.detector;
18+
19+
import org.junit.jupiter.api.BeforeEach;
20+
import org.junit.jupiter.api.Test;
21+
import org.mockito.Mockito;
22+
23+
import org.springframework.experimental.boot.server.exec.main.SpringBootApplicationMain;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.mockito.ArgumentMatchers.any;
27+
28+
class JarLauncherDetectorTests {
29+
30+
@BeforeEach
31+
void setUp() {
32+
MockJarLauncher.reset();
33+
}
34+
35+
@Test
36+
void whenJarLauncherInLoaderLaunchPackage() {
37+
try (var mocked = Mockito.mockStatic(JarLauncherDetector.class)) {
38+
mocked.when(() -> JarLauncherDetector.main(any())).thenCallRealMethod();
39+
mocked.when(() -> JarLauncherDetector.loadClass(any())).thenThrow(new ClassNotFoundException());
40+
mocked.when(() -> JarLauncherDetector.loadClass("org.springframework.boot.loader.launch.JarLauncher"))
41+
.thenReturn(MockJarLauncher.class);
42+
43+
var args = new String[] { "one", "two" };
44+
JarLauncherDetector.main(args);
45+
46+
assertThat(MockJarLauncher.callCount).isEqualTo(1);
47+
assertThat(MockJarLauncher.callArgs).isSameAs(args);
48+
}
49+
}
50+
51+
@Test
52+
void whenJarLauncherInLoaderPackage() {
53+
try (var mocked = Mockito.mockStatic(JarLauncherDetector.class)) {
54+
mocked.when(() -> JarLauncherDetector.main(any())).thenCallRealMethod();
55+
mocked.when(() -> JarLauncherDetector.loadClass(any())).thenThrow(new ClassNotFoundException());
56+
mocked.when(() -> JarLauncherDetector.loadClass("org.springframework.boot.loader.JarLauncher"))
57+
.thenReturn(MockJarLauncher.class);
58+
59+
var args = new String[] { "one", "two" };
60+
JarLauncherDetector.main(args);
61+
62+
assertThat(MockJarLauncher.callCount).isEqualTo(1);
63+
assertThat(MockJarLauncher.callArgs).isSameAs(args);
64+
}
65+
}
66+
67+
@Test
68+
void whenJarLauncherMissing() {
69+
try (var mocked = Mockito.mockStatic(JarLauncherDetector.class);
70+
var mockedSpringBootMain = Mockito.mockStatic(SpringBootApplicationMain.class)) {
71+
mocked.when(() -> JarLauncherDetector.main(any())).thenCallRealMethod();
72+
mocked.when(() -> JarLauncherDetector.loadClass(any())).thenThrow(new ClassNotFoundException());
73+
74+
final var callArgs = new String[] { "one", "two" };
75+
JarLauncherDetector.main(callArgs);
76+
77+
mockedSpringBootMain.verify(() -> SpringBootApplicationMain.main(callArgs));
78+
}
79+
}
80+
81+
public static final class MockJarLauncher {
82+
83+
static int callCount = 0;
84+
85+
static String[] callArgs = null;
86+
87+
public static void main(String[] args) {
88+
callCount++;
89+
callArgs = args;
90+
}
91+
92+
private static void reset() {
93+
callCount = 0;
94+
callArgs = null;
95+
}
96+
97+
}
98+
99+
}

0 commit comments

Comments
 (0)