Skip to content

Commit

Permalink
Add JarLauncherDetector main class
Browse files Browse the repository at this point in the history
- When executing a Spring Boot fat jar, detects the correct JarLauncher
  to use based on what is available.
  • Loading branch information
Kehrlann authored and rwinch committed Jun 25, 2024
1 parent 7f3c940 commit 481aaeb
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 8 deletions.
1 change: 1 addition & 0 deletions config/checkstyle/checkstyle-suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
<!-- Ignore third-party code -->
<suppress files="LoggingMavenRepositoryListener\.java|LoggingMavenTransferListener\.java" checks=".*"/>
<suppress files="SpringBootApplicationMain\.java" checks=".*"/>
<suppress files="JarLauncherDetector\.java" checks=".*"/>
</suppressions>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
Expand Down Expand Up @@ -37,6 +37,7 @@
* application.properties respectively.
*
* @author Rob Winch
* @author Daniel Garnier-Moiroux
*/
public class CommonsExecWebServerFactoryBean
implements FactoryBean<CommonsExecWebServer>, DisposableBean, BeanNameAware {
Expand All @@ -49,16 +50,17 @@ public class CommonsExecWebServerFactoryBean

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

private String mainClass = "org.springframework.boot.loader.JarLauncher";
private String mainClass = "org.springframework.experimental.boot.server.exec.detector.JarLauncherDetector";

private File applicationPortFile = createApplicationPortFile();

private CommonsExecWebServer webServer;

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

public static CommonsExecWebServerFactoryBean builder() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2012-2024 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
*
* https://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.experimental.boot.server.exec.detector;

import java.lang.reflect.InvocationTargetException;

import org.springframework.experimental.boot.server.exec.main.SpringBootApplicationMain;

/**
* Detect which JarLauncher main class to use, and call its {@code main(String[] args)}
* methods.
* <p>
* The location depends on the Boot version. Prior to 3.2, it was in the
* {@code org.springframework.boot.loader} package. In 3.2, it was moved to the
* {@code org.springframework.boot.loader.launch} package.
*
* @author Daniel Garnier-Moiroux
*/
public class JarLauncherDetector {

public static void main(String[] args) {
try {
// Boot >= 3.2
Class<?> jarLauncher = loadClass("org.springframework.boot.loader.launch.JarLauncher");
var mainMethod = jarLauncher.getMethod("main", String[].class);
mainMethod.invoke(null, (Object) args);
return;
}
catch (ClassNotFoundException ignored) {
// no-op
}
catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
// TODO: log?
throw new RuntimeException(ex);
}

try {
// Boot < 3.2
Class<?> jarLauncher = loadClass("org.springframework.boot.loader.JarLauncher");
var mainMethod = jarLauncher.getMethod("main", String[].class);
mainMethod.invoke(null, (Object) args);
return;
}
catch (ClassNotFoundException ignored) {
// no-op
}
catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException ex) {
// TODO: log?
throw new RuntimeException(ex);
}

SpringBootApplicationMain.main(args);
}

// Helpful for testing, because Class.forName cannot be mocked
public static Class<?> loadClass(String className) throws ClassNotFoundException {
return Class.forName(className);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2012-2024 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
*
* https://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.
*/

/**
* Provides a default main class to run detect the correct Spring Boot JarLauncher.
*/
package org.springframework.experimental.boot.server.exec.detector;
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/

/**
* Provides a default main class to run Spring Boot applications against. This is
* intentionally left otherwise empty to avoid scanning classes unnecessarily.
* Provides a main class which detects which Spring Boot JarLauncher to use. It also
* provides a default class to run Spring Boot applications against. This is intentionally
* left otherwise empty to avoid scanning classes unnecessarily.
*/
package org.springframework.experimental.boot.server.exec.main;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
Expand All @@ -17,8 +17,10 @@
package org.springframework.experimental.boot.server.exec;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

Expand All @@ -37,8 +39,8 @@ void classpathContainsSpringFactories() throws Exception {
int index = args.indexOf("-classpath");
assertThat(index).isGreaterThanOrEqualTo(0);
assertThat(args).hasSizeGreaterThan(index + 1);
String classpath = args.get(index + 1);
URLClassLoader loader = new URLClassLoader(new URL[] { new File(classpath).toURI().toURL() }, null);
String classpathArgs = args.get(index + 1);
var loader = getClassLoaderFromArgs(classpathArgs);
assertThat(loader.findResource("META-INF/spring.factories")).isNotNull();
server.destroy();
}
Expand Down Expand Up @@ -75,4 +77,38 @@ void mainClassWhenNull() {
.isThrownBy(() -> CommonsExecWebServerFactoryBean.builder().mainClass(mainClass));
}

@Test
void usesJarLauncherwhenNoMainClassDefined() throws Exception {
CommonsExecWebServer webServer = CommonsExecWebServerFactoryBean.builder().getObject();
String[] args = webServer.getCommandLine().getArguments();
assertThat(args[args.length - 1])
.isEqualTo("org.springframework.experimental.boot.server.exec.detector.JarLauncherDetector");
}

@Test
void doesNotAddJarLauncherDetectorLauncherDetectorWhenMainClassDefined() throws Exception {
String mainClass = "example.Main";
CommonsExecWebServer server = CommonsExecWebServerFactoryBean.builder().mainClass(mainClass).getObject();
List<String> args = Arrays.asList(server.getCommandLine().getArguments());
int index = args.indexOf("-classpath");
assertThat(index).isGreaterThanOrEqualTo(0);
assertThat(args).hasSizeGreaterThan(index + 1);
String classpathArgs = args.get(index + 1);
URLClassLoader loader = getClassLoaderFromArgs(classpathArgs);
assertThat(
loader.findResource("org.springframework.experimental.boot.server.exec.detector.JarLauncherDetector"))
.isNull();
server.destroy();
}

private static URLClassLoader getClassLoaderFromArgs(String classpathArgs) throws MalformedURLException {
var paths = new ArrayList<URL>();
for (String path : classpathArgs.split(":")) {
var url = new File(path).toURI().toURL();
paths.add(url);
}
URLClassLoader loader = new URLClassLoader(paths.toArray(new URL[] {}), null);
return loader;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Copyright 2012-2024 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
*
* https://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.experimental.boot.server.exec.detector;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import org.springframework.experimental.boot.server.exec.main.SpringBootApplicationMain;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;

class JarLauncherDetectorTests {

@BeforeEach
void setUp() {
MockJarLauncher.reset();
}

@Test
void whenJarLauncherInLoaderLaunchPackage() {
try (var mocked = Mockito.mockStatic(JarLauncherDetector.class)) {
mocked.when(() -> JarLauncherDetector.main(any())).thenCallRealMethod();
mocked.when(() -> JarLauncherDetector.loadClass(any())).thenThrow(new ClassNotFoundException());
mocked.when(() -> JarLauncherDetector.loadClass("org.springframework.boot.loader.launch.JarLauncher"))
.thenReturn(MockJarLauncher.class);

var args = new String[] { "one", "two" };
JarLauncherDetector.main(args);

assertThat(MockJarLauncher.callCount).isEqualTo(1);
assertThat(MockJarLauncher.callArgs).isSameAs(args);
}
}

@Test
void whenJarLauncherInLoaderPackage() {
try (var mocked = Mockito.mockStatic(JarLauncherDetector.class)) {
mocked.when(() -> JarLauncherDetector.main(any())).thenCallRealMethod();
mocked.when(() -> JarLauncherDetector.loadClass(any())).thenThrow(new ClassNotFoundException());
mocked.when(() -> JarLauncherDetector.loadClass("org.springframework.boot.loader.JarLauncher"))
.thenReturn(MockJarLauncher.class);

var args = new String[] { "one", "two" };
JarLauncherDetector.main(args);

assertThat(MockJarLauncher.callCount).isEqualTo(1);
assertThat(MockJarLauncher.callArgs).isSameAs(args);
}
}

@Test
void whenJarLauncherMissing() {
try (var mocked = Mockito.mockStatic(JarLauncherDetector.class);
var mockedSpringBootMain = Mockito.mockStatic(SpringBootApplicationMain.class)) {
mocked.when(() -> JarLauncherDetector.main(any())).thenCallRealMethod();
mocked.when(() -> JarLauncherDetector.loadClass(any())).thenThrow(new ClassNotFoundException());

final var callArgs = new String[] { "one", "two" };
JarLauncherDetector.main(callArgs);

mockedSpringBootMain.verify(() -> SpringBootApplicationMain.main(callArgs));
}
}

public static final class MockJarLauncher {

static int callCount = 0;

static String[] callArgs = null;

public static void main(String[] args) {
callCount++;
callArgs = args;
}

private static void reset() {
callCount = 0;
callArgs = null;
}

}

}

0 comments on commit 481aaeb

Please sign in to comment.