Add fork option for mvn spring-boot:run

Update RunMojo to include a `fork` option and change the default
to only fork if agent or jvmArguments are specified.

Fixes gh-1412
pull/1780/merge
David Liu 10 years ago committed by Phillip Webb
parent 026b89f58c
commit 9bf1c89750

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>run-fork</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<fork>true</fork>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,9 @@
package org.test;
public class SampleApplication {
public static void main(String[] args) {
System.out.println("I haz been run");
}
}

@ -0,0 +1,3 @@
def file = new File(basedir, "build.log")
return file.text.contains("I haz been run")

@ -18,8 +18,10 @@ package org.springframework.boot.maven;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource; import java.security.CodeSource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -49,6 +51,7 @@ import org.springframework.boot.loader.tools.RunProcess;
* *
* @author Phillip Webb * @author Phillip Webb
* @author Stephane Nicoll * @author Stephane Nicoll
* @author David Liu
*/ */
@Mojo(name = "run", requiresProject = true, defaultPhase = LifecyclePhase.VALIDATE, requiresDependencyResolution = ResolutionScope.TEST) @Mojo(name = "run", requiresProject = true, defaultPhase = LifecyclePhase.VALIDATE, requiresDependencyResolution = ResolutionScope.TEST)
@Execute(phase = LifecyclePhase.TEST_COMPILE) @Execute(phase = LifecyclePhase.TEST_COMPILE)
@ -73,7 +76,8 @@ public class RunMojo extends AbstractDependencyFilterMojo {
private boolean addResources; 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 * @since 1.0
*/ */
@Parameter(property = "run.agent") @Parameter(property = "run.agent")
@ -126,6 +130,14 @@ public class RunMojo extends AbstractDependencyFilterMojo {
@Parameter(defaultValue = "${project.build.outputDirectory}", required = true) @Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
private File classesDirectory; 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 @Override
public void execute() throws MojoExecutionException, MojoFailureException { public void execute() throws MojoExecutionException, MojoFailureException {
final String startClassName = getStartClass(); final String startClassName = getStartClass();
@ -156,6 +168,16 @@ public class RunMojo extends AbstractDependencyFilterMojo {
} }
private void run(String startClassName) throws MojoExecutionException { 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<String> args = new ArrayList<String>(); List<String> args = new ArrayList<String>();
addAgents(args); addAgents(args);
addJvmArgs(args); addJvmArgs(args);
@ -166,11 +188,21 @@ public class RunMojo extends AbstractDependencyFilterMojo {
new RunProcess(new JavaExecutable().toString()).run(args new RunProcess(new JavaExecutable().toString()).run(args
.toArray(new String[args.size()])); .toArray(new String[args.size()]));
} }
catch (Exception e) { catch (Exception ex) {
throw new MojoExecutionException("Could not exec java", e); 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<String> args) { private void addAgents(List<String> args) {
findAgent(); findAgent();
if (this.agent != null) { if (this.agent != null) {
@ -287,6 +319,27 @@ public class RunMojo extends AbstractDependencyFilterMojo {
getLog().debug(sb.toString().trim()); 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 { private static class TestArtifactFilter extends AbstractArtifactFeatureFilter {
public TestArtifactFilter() { public TestArtifactFilter() {
super("", Artifact.SCOPE_TEST); 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);
}
}
}
} }

@ -99,9 +99,13 @@ Usage
mvn spring-boot:run mvn spring-boot:run
--- ---
The application is forked in a separate process. If you need to specify some JVM arguments By default the application is executed directly from the Maven JVM. If you need to run
(i.e. for debugging purposes), you can use the <<<jvmArguments>>> parameter, see in a forked process you can use the 'fork' option. Forking will also occur if the
{{{./examples/run-debug.html}Debug the application}} for more details. 'jvmArguments' or 'agent' options are specified.
If you need to specify some JVM arguments (i.e. for debugging purposes), you can use
the <<<jvmArguments>>> parameter, see {{{./examples/run-debug.html}Debug the application}}
for more details.
By default, any <<src/main/resources>> folder will be added to the application classpath By default, any <<src/main/resources>> folder will be added to the application classpath
when you run the application and any duplicate found in <<target/classes>> will be when you run the application and any duplicate found in <<target/classes>> will be
@ -135,4 +139,4 @@ mvn spring-boot:run
In order to be consistent with the <<<repackage>>> goal, the <<<run>>> goal builds the classpath In order to be consistent with the <<<repackage>>> goal, the <<<run>>> goal builds the classpath
in such a way that any dependency that is excluded in the plugin's configuration gets excluded 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 from the classpath as well. See {{{./examples/exclude-dependency.html}Exclude a dependency}} for
more details. more details.

Loading…
Cancel
Save