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.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<String> args = new ArrayList<String>();
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<String> 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);
}
}
}
}

@ -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 <<<jvmArguments>>> 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 <<<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
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 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.
more details.

Loading…
Cancel
Save