From 73a42050d63403fc6791ac3b55ceca747637ca85 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 22 Jan 2020 01:08:45 -0800 Subject: [PATCH] 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 --- .../boot/loader/ExplodedURLClassLoader.java | 47 ++++++++++ .../boot/loader/LaunchedURLClassLoader.java | 13 ++- .../springframework/boot/loader/Launcher.java | 15 ++-- .../boot/loader/MainMethodRunner.java | 3 +- .../boot/loader/jarmode/JarMode.java | 42 +++++++++ .../boot/loader/jarmode/JarModeLauncher.java | 53 ++++++++++++ .../boot/loader/jarmode/TestJarMode.java | 38 ++++++++ .../loader/jarmode/LauncherJarModeTests.java | 86 +++++++++++++++++++ .../test/resources/META-INF/spring.factories | 2 + 9 files changed, 291 insertions(+), 8 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExplodedURLClassLoader.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExplodedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExplodedURLClassLoader.java new file mode 100644 index 0000000000..c6c6c9225e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/ExplodedURLClassLoader.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2020 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.boot.loader; + +import java.net.URL; +import java.net.URLClassLoader; + +/** + * {@link URLClassLoader} used for exploded archives. + * + * @author Phillip Webb + */ +class ExplodedURLClassLoader extends URLClassLoader { + + ExplodedURLClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + try { + Class result = findClass(name); + if (resolve) { + resolveClass(result); + } + return result; + } + catch (ClassNotFoundException ex) { + } + return super.loadClass(name, resolve); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java index 9a6ca13269..e7efb84480 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/LaunchedURLClassLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -75,6 +75,17 @@ public class LaunchedURLClassLoader extends URLClassLoader { @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name.startsWith("org.springframework.boot.loader.jarmode.")) { + try { + Class result = findClass(name); + if (resolve) { + resolveClass(result); + } + return result; + } + catch (ClassNotFoundException ex) { + } + } Handler.setUseFastConnectionExceptions(true); try { try { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java index 5fc0a0fb43..aef84ae564 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/Launcher.java @@ -19,7 +19,6 @@ package org.springframework.boot.loader; import java.io.File; import java.net.URI; import java.net.URL; -import java.net.URLClassLoader; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.ArrayList; @@ -41,6 +40,8 @@ import org.springframework.boot.loader.jar.JarFile; */ public abstract class Launcher { + private static final String JAR_MODE_LAUNCHER = "org.springframework.boot.loader.jarmode.JarModeLauncher"; + /** * Launch the application. This method is the initial entry point that should be * called by a subclass {@code public static void main(String[] args)} method. @@ -52,7 +53,9 @@ public abstract class Launcher { JarFile.registerUrlProtocolHandler(); } ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator()); - launch(args, getMainClass(), classLoader); + String jarMode = System.getProperty("jarmode"); + String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass(); + launch(args, launchClass, classLoader); } /** @@ -94,19 +97,19 @@ public abstract class Launcher { if (supportsNestedJars()) { return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); } - return new URLClassLoader(urls, getClass().getClassLoader()); + return new ExplodedURLClassLoader(urls, getClass().getClassLoader()); } /** * Launch the application given the archive file and a fully configured classloader. * @param args the incoming arguments - * @param mainClass the main class to run + * @param launchClass the launch class to run * @param classLoader the classloader * @throws Exception if the launch fails */ - protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception { + protected void launch(String[] args, String launchClass, ClassLoader classLoader) throws Exception { Thread.currentThread().setContextClassLoader(classLoader); - createMainMethodRunner(mainClass, args, classLoader).run(); + createMainMethodRunner(launchClass, args, classLoader).run(); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java index 23edb4e726..b540ccbb8e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/MainMethodRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -45,6 +45,7 @@ public class MainMethodRunner { public void run() throws Exception { Class mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName); Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); + mainMethod.setAccessible(true); mainMethod.invoke(null, new Object[] { this.args }); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java new file mode 100644 index 0000000000..c711e206f5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarMode.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 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.boot.loader.jarmode; + +/** + * Interface registered in {@code spring.factories} to provides extended 'jarmode' + * support. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public interface JarMode { + + /** + * Returns if this accepts and can run the given mode. + * @param mode the mode to check + * @return if this instance accepts the mode + */ + boolean accepts(String mode); + + /** + * Run the jar in the given mode. + * @param mode the mode to use + * @param args any program arguments + */ + void run(String mode, String[] args); + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java new file mode 100644 index 0000000000..42a89a50a3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/JarModeLauncher.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2020 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.boot.loader.jarmode; + +import java.util.List; + +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.util.ClassUtils; + +/** + * Delegate class used to launch the fat jar in a specific mode. + * + * @author Phillip Webb + * @since 2.3.0 + */ +public final class JarModeLauncher { + + static final String DISABLE_SYSTEM_EXIT = JarModeLauncher.class.getName() + ".DISABLE_SYSTEM_EXIT"; + + private JarModeLauncher() { + } + + public static void main(String[] args) { + String mode = System.getProperty("jarmode"); + List candidates = SpringFactoriesLoader.loadFactories(JarMode.class, + ClassUtils.getDefaultClassLoader()); + for (JarMode candidate : candidates) { + if (candidate.accepts(mode)) { + candidate.run(mode, args); + return; + } + } + System.err.println("Unsupported jarmode '" + mode + "'"); + if (!Boolean.getBoolean(DISABLE_SYSTEM_EXIT)) { + System.exit(1); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java new file mode 100644 index 0000000000..6a6e83ff23 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/jarmode/TestJarMode.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2020 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.boot.loader.jarmode; + +import java.util.Arrays; + +/** + * {@link JarMode} for testing. + * + * @author Phillip Webb + */ +class TestJarMode implements JarMode { + + @Override + public boolean accepts(String mode) { + return "test".equals(mode); + } + + @Override + public void run(String mode, String[] args) { + System.out.println("running in " + mode + " jar mode " + Arrays.asList(args)); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java new file mode 100644 index 0000000000..93179bad6f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jarmode/LauncherJarModeTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2020 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.boot.loader.jarmode; + +import java.util.Collections; +import java.util.Iterator; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.loader.Launcher; +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Launcher} with jar mode support. + * + * @author Phillip Webb + */ +@ExtendWith(OutputCaptureExtension.class) +class LauncherJarModeTests { + + @BeforeEach + void setup() { + System.setProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT, "true"); + } + + @AfterEach + void cleanup() { + System.clearProperty("jarmode"); + System.clearProperty(JarModeLauncher.DISABLE_SYSTEM_EXIT); + } + + @Test + void launchWhenJarModePropertyIsSetLaunchesJarMode(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "test"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("running in test jar mode [boot]"); + } + + @Test + void launchWhenJarModePropertyIsNotAcceptedThrowsException(CapturedOutput out) throws Exception { + System.setProperty("jarmode", "idontexist"); + new TestLauncher().launch(new String[] { "boot" }); + assertThat(out).contains("Unsupported jarmode 'idontexist'"); + } + + private static class TestLauncher extends Launcher { + + @Override + protected String getMainClass() throws Exception { + throw new IllegalStateException("Should not be called"); + } + + @Override + protected Iterator getClassPathArchivesIterator() throws Exception { + return Collections.emptyIterator(); + } + + @Override + protected void launch(String[] args) throws Exception { + super.launch(args); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories new file mode 100644 index 0000000000..a140aabf17 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.loader.jarmode.JarMode=\ +org.springframework.boot.loader.jarmode.TestJarMode \ No newline at end of file