diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/pom.xml b/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/pom.xml new file mode 100644 index 0000000000..651f999e49 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/pom.xml @@ -0,0 +1,31 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-fork + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + package + + run + + + true + + + + + + + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/src/main/java/org/test/SampleApplication.java b/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/src/main/java/org/test/SampleApplication.java new file mode 100644 index 0000000000..30c4f3246d --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,9 @@ +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + System.out.println("I haz been run"); + } + +} diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/verify.groovy b/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/verify.groovy new file mode 100644 index 0000000000..841c4a97de --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/run-fork/verify.groovy @@ -0,0 +1,3 @@ +def file = new File(basedir, "build.log") +return file.text.contains("I haz been run") + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java index 52cac1d115..ec7dd626ab 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java @@ -18,8 +18,10 @@ package org.springframework.boot.maven; import java.io.File; import java.io.IOException; +import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLClassLoader; import java.security.CodeSource; import java.util.ArrayList; import java.util.Arrays; @@ -49,6 +51,7 @@ import org.springframework.boot.loader.tools.RunProcess; * * @author Phillip Webb * @author Stephane Nicoll + * @author David Liu */ @Mojo(name = "run", requiresProject = true, defaultPhase = LifecyclePhase.VALIDATE, requiresDependencyResolution = ResolutionScope.TEST) @Execute(phase = LifecyclePhase.TEST_COMPILE) @@ -73,7 +76,8 @@ public class RunMojo extends AbstractDependencyFilterMojo { private boolean addResources; /** - * Path to agent jar. + * Path to agent jar. NOTE: the use of agents means that processes will be started by + * forking a new JVM. * @since 1.0 */ @Parameter(property = "run.agent") @@ -126,6 +130,14 @@ public class RunMojo extends AbstractDependencyFilterMojo { @Parameter(defaultValue = "${project.build.outputDirectory}", required = true) private File classesDirectory; + /** + * Flag to indicate if the run processes should be forked. By default process forking + * is only used if an agent or jvmArguments are specified. + * @since 1.2 + */ + @Parameter(property = "fork", defaultValue = "false") + private boolean fork; + @Override public void execute() throws MojoExecutionException, MojoFailureException { final String startClassName = getStartClass(); @@ -156,6 +168,16 @@ public class RunMojo extends AbstractDependencyFilterMojo { } private void run(String startClassName) throws MojoExecutionException { + if (this.fork || (this.agent != null && this.agent.length > 0) + || (this.jvmArguments != null && this.jvmArguments.length() > 0)) { + runWithForkedJvm(startClassName); + } + else { + runWithMavenJvm(startClassName); + } + } + + private void runWithForkedJvm(String startClassName) throws MojoExecutionException { List args = new ArrayList(); addAgents(args); addJvmArgs(args); @@ -166,11 +188,21 @@ public class RunMojo extends AbstractDependencyFilterMojo { new RunProcess(new JavaExecutable().toString()).run(args .toArray(new String[args.size()])); } - catch (Exception e) { - throw new MojoExecutionException("Could not exec java", e); + catch (Exception ex) { + throw new MojoExecutionException("Could not exec java", ex); } } + private void runWithMavenJvm(String startClassName) throws MojoExecutionException { + IsolatedThreadGroup threadGroup = new IsolatedThreadGroup(startClassName); + Thread launchThread = new Thread(threadGroup, new LaunchRunner(startClassName, + this.arguments), startClassName + ".main()"); + launchThread.setContextClassLoader(new URLClassLoader(getClassPathUrls())); + launchThread.start(); + join(threadGroup); + threadGroup.rethrowUncaughtException(); + } + private void addAgents(List args) { findAgent(); if (this.agent != null) { @@ -287,6 +319,27 @@ public class RunMojo extends AbstractDependencyFilterMojo { getLog().debug(sb.toString().trim()); } + private void join(ThreadGroup threadGroup) { + boolean hasNonDaemonThreads; + do { + hasNonDaemonThreads = false; + Thread[] threads = new Thread[threadGroup.activeCount()]; + threadGroup.enumerate(threads); + for (Thread thread : threads) { + if (thread != null && !thread.isDaemon()) { + try { + hasNonDaemonThreads = true; + thread.join(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } + } + } + while (hasNonDaemonThreads); + } + private static class TestArtifactFilter extends AbstractArtifactFeatureFilter { public TestArtifactFilter() { super("", Artifact.SCOPE_TEST); @@ -298,4 +351,73 @@ public class RunMojo extends AbstractDependencyFilterMojo { } } + /** + * Isolated {@link ThreadGroup} to capture uncaught exceptions. + */ + class IsolatedThreadGroup extends ThreadGroup { + + private Throwable exception; + + public IsolatedThreadGroup(String name) { + super(name); + } + + @Override + public void uncaughtException(Thread thread, Throwable ex) { + if (!(ex instanceof ThreadDeath)) { + synchronized (this) { + this.exception = (this.exception == null ? ex : this.exception); + } + getLog().warn(ex); + } + } + + public synchronized void rethrowUncaughtException() throws MojoExecutionException { + if (this.exception != null) { + throw new MojoExecutionException("An exception occured while running. " + + this.exception.getMessage(), this.exception); + } + } + + } + + /** + * Runner used to launch the application. + */ + class LaunchRunner implements Runnable { + + private final String startClassName; + private final String[] args; + + public LaunchRunner(String startClassName, String... args) { + this.startClassName = startClassName; + this.args = (args != null ? args : new String[] {}); + } + + @Override + public void run() { + Thread thread = Thread.currentThread(); + ClassLoader classLoader = thread.getContextClassLoader(); + try { + Class startClass = classLoader.loadClass(this.startClassName); + Method mainMethod = startClass.getMethod("main", + new Class[] { String[].class }); + if (!mainMethod.isAccessible()) { + mainMethod.setAccessible(true); + } + mainMethod.invoke(null, new Object[] { this.args }); + } + catch (NoSuchMethodException ex) { + Exception wrappedEx = new Exception( + "The specified mainClass doesn't contain a " + + "main method with appropriate signature.", ex); + thread.getThreadGroup().uncaughtException(thread, wrappedEx); + } + catch (Exception ex) { + thread.getThreadGroup().uncaughtException(thread, ex); + } + } + + } + } diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm b/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm index a08c0449b1..f4e983e67e 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm +++ b/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm @@ -99,9 +99,13 @@ Usage mvn spring-boot:run --- - The application is forked in a separate process. If you need to specify some JVM arguments - (i.e. for debugging purposes), you can use the <<>> parameter, see - {{{./examples/run-debug.html}Debug the application}} for more details. + By default the application is executed directly from the Maven JVM. If you need to run + in a forked process you can use the 'fork' option. Forking will also occur if the + 'jvmArguments' or 'agent' options are specified. + + If you need to specify some JVM arguments (i.e. for debugging purposes), you can use + the <<>> parameter, see {{{./examples/run-debug.html}Debug the application}} + for more details. By default, any <> folder will be added to the application classpath when you run the application and any duplicate found in <> will be @@ -135,4 +139,4 @@ mvn spring-boot:run In order to be consistent with the <<>> goal, the <<>> goal builds the classpath in such a way that any dependency that is excluded in the plugin's configuration gets excluded from the classpath as well. See {{{./examples/exclude-dependency.html}Exclude a dependency}} for - more details. \ No newline at end of file + more details.