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.