Add start/stop goals to maven plugin

SpringApplicationLifecycle provides basic lifecycle operations on the
current Spring Boot application (that is checking if the application has
fully started and gracefully terminate the app). It can be registered as
an MBean of the platform MBean server if a specific property is set.

The Maven plugin uses that MBean to check that the application is ready
before ending the "start" phase. It uses it to trigger a proper shutdown
of the application during the "stop" phase.

If the process has to be forked, the platform MBean server is exposed on
a configurable port so that the maven plugin can connect to it.

Such change permits the maven plugin to integrate a classical integration
test scenario where the "start" goal is invoked during the
pre-integration phase and the "stop" goal during the post-integration
phase.

Closes gh-2525
pull/3013/head
Stephane Nicoll 10 years ago
parent 129c24926e
commit e0dfe9fb86

@ -0,0 +1,72 @@
/*
* Copyright 2012-2015 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
*
* http://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.autoconfigure.context;
import javax.management.MalformedObjectNameException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.context.SpringApplicationLifecycleMXBean;
import org.springframework.boot.context.SpringApplicationLifecycleRegistrar;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.jmx.export.MBeanExporter;
/**
* Register a JMX component that allows to manage the lifecycle of the current
* application. Intended for internal use only.
*
* @author Stephane Nicoll
* @since 1.3.0
* @see SpringApplicationLifecycleMXBean
*/
@Configuration
@AutoConfigureAfter(JmxAutoConfiguration.class)
@ConditionalOnProperty(value = "spring.context.lifecycle.enabled", havingValue = "true", matchIfMissing = false)
class SpringApplicationLifecycleAutoConfiguration {
/**
* The property to use to customize the {@code ObjectName} of the application lifecycle mbean.
*/
static final String JMX_NAME_PROPERTY = "spring.context.lifecycle.jmx-name";
/**
* The default {@code ObjectName} of the application lifecycle mbean.
*/
static final String DEFAULT_JMX_NAME = "org.springframework.boot:type=Lifecycle,name=springApplicationLifecycle";
@Autowired(required = false)
private MBeanExporter mbeanExporter;
@Autowired
private Environment environment;
@Bean
public SpringApplicationLifecycleRegistrar springApplicationLifecycleRegistrar()
throws MalformedObjectNameException {
String jmxName = this.environment.getProperty(JMX_NAME_PROPERTY, DEFAULT_JMX_NAME);
if (mbeanExporter != null) { // Make sure to not register that MBean twice
mbeanExporter.addExcludedBean(jmxName);
}
return new SpringApplicationLifecycleRegistrar(jmxName);
}
}

@ -11,6 +11,7 @@ org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\
org.springframework.boot.autoconfigure.context.SpringApplicationLifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\

@ -0,0 +1,128 @@
/*
* Copyright 2012-2015 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
*
* http://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.autoconfigure.context;
import java.lang.management.ManagementFactory;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
/**
* Tests for {@link SpringApplicationLifecycleAutoConfiguration}.
*
* @author Stephane Nicoll
*/
public class SpringApplicationLifecycleAutoConfigurationTests {
public static final String ENABLE_LIFECYCLE_PROP = "spring.context.lifecycle.enabled=true";
@Rule
public final ExpectedException thrown = ExpectedException.none();
private AnnotationConfigApplicationContext context;
private MBeanServer mBeanServer;
@Before
public void setup() throws MalformedObjectNameException {
this.mBeanServer = ManagementFactory.getPlatformMBeanServer();
}
@After
public void tearDown() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void notRegisteredByDefault() throws MalformedObjectNameException, InstanceNotFoundException {
load();
thrown.expect(InstanceNotFoundException.class);
this.mBeanServer.getObjectInstance(createDefaultObjectName());
}
@Test
public void registeredWithProperty() throws Exception {
load(ENABLE_LIFECYCLE_PROP);
ObjectName objectName = createDefaultObjectName();
ObjectInstance objectInstance = this.mBeanServer.getObjectInstance(objectName);
assertNotNull("Lifecycle bean should have been registered", objectInstance);
}
@Test
public void registerWithCustomJmxName() throws InstanceNotFoundException {
String customJmxName = "org.acme:name=FooBar";
System.setProperty(SpringApplicationLifecycleAutoConfiguration.JMX_NAME_PROPERTY, customJmxName);
try {
load(ENABLE_LIFECYCLE_PROP);
try {
this.mBeanServer.getObjectInstance(createObjectName(customJmxName));
}
catch (InstanceNotFoundException e) {
fail("lifecycle MBean should have been exposed with custom name");
}
thrown.expect(InstanceNotFoundException.class); // Should not be exposed
this.mBeanServer.getObjectInstance(createDefaultObjectName());
}
finally {
System.clearProperty(SpringApplicationLifecycleAutoConfiguration.JMX_NAME_PROPERTY);
}
}
private ObjectName createDefaultObjectName() {
return createObjectName(SpringApplicationLifecycleAutoConfiguration.DEFAULT_JMX_NAME);
}
private ObjectName createObjectName(String jmxName) {
try {
return new ObjectName(jmxName);
}
catch (MalformedObjectNameException e) {
throw new IllegalStateException("Invalid jmx name " + jmxName, e);
}
}
private void load(String... environment) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
EnvironmentTestUtils.addEnvironment(applicationContext, environment);
applicationContext.register(JmxAutoConfiguration.class, SpringApplicationLifecycleAutoConfiguration.class);
applicationContext.refresh();
this.context = applicationContext;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@ -48,7 +48,7 @@ class RunProcessCommand extends AbstractCommand {
protected ExitStatus run(Collection<String> args) throws IOException {
this.process = new RunProcess(this.command);
int code = this.process.run(args.toArray(new String[args.size()]));
int code = this.process.run(true, args.toArray(new String[args.size()]));
if (code == 0) {
return ExitStatus.OK;
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@ -31,6 +31,7 @@ import org.springframework.util.ReflectionUtils;
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 1.1.0
*/
public class RunProcess {
@ -50,11 +51,18 @@ public class RunProcess {
this.command = command;
}
public int run(String... args) throws IOException {
return run(Arrays.asList(args));
public int run(boolean waitForProcess, String... args) throws IOException {
return run(waitForProcess, Arrays.asList(args));
}
protected int run(Collection<String> args) throws IOException {
/**
* Kill this process.
*/
public void kill() {
doKill();
}
protected int run(boolean waitForProcess, Collection<String> args) throws IOException {
ProcessBuilder builder = new ProcessBuilder(this.command);
builder.command().addAll(args);
builder.redirectErrorStream(true);
@ -71,17 +79,22 @@ public class RunProcess {
handleSigInt();
}
});
try {
return process.waitFor();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return 1;
if (waitForProcess) {
try {
return process.waitFor();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return 1;
}
}
return 5;
}
finally {
this.endTime = System.currentTimeMillis();
this.process = null;
if (waitForProcess) {
this.endTime = System.currentTimeMillis();
this.process = null;
}
}
}
@ -163,7 +176,11 @@ public class RunProcess {
if (hasJustEnded()) {
return true;
}
return doKill();
}
private boolean doKill() {
// destroy the running process
Process process = this.process;
if (process != null) {

@ -0,0 +1,37 @@
<?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>start-stop-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>
<id>pre-integration-test</id>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>post-integration-test</id>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,79 @@
/*
* Copyright 2012-2015 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
*
* http://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.test;
import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
import javax.management.ObjectName;
/**
* This sample app simulates the JMX Mbean that is exposed by the Spring
* Boot application.
*/
public class SampleApplication {
private static final Object lock = new Object();
public static void main(String[] args) throws Exception {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("org.springframework.boot:type=Lifecycle,name=springApplicationLifecycle");
SpringApplicationLifecycle mbean = new SpringApplicationLifecycle();
mbs.registerMBean(mbean, name);
// Flag the app as ready
mbean.ready = true;
int waitAttempts = 0;
while (!mbean.shutdownInvoked) {
if (waitAttempts > 10) {
throw new IllegalStateException("Shutdown should have been invoked by now");
}
synchronized (lock) {
lock.wait(250);
}
waitAttempts++;
}
System.out.println("Application has terminated gracefully");
}
public interface SpringApplicationLifecycleMXBean {
boolean isReady();
void shutdown();
}
static class SpringApplicationLifecycle implements SpringApplicationLifecycleMXBean {
private boolean ready;
private boolean shutdownInvoked;
@Override
public boolean isReady() {
return this.ready;
}
@Override
public void shutdown() {
this.shutdownInvoked = true;
System.out.println("Shutdown requested");
}
}
}

@ -0,0 +1,6 @@
import static org.junit.Assert.assertTrue
def file = new File(basedir, "build.log")
assertTrue 'Shutdown should have been invoked', file.text.contains("Shutdown requested")
assertTrue 'Application should have terminated', file.text.contains("Application has terminated gracefully")

@ -0,0 +1,34 @@
<?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>start-stop</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>
<id>pre-integration-test</id>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>post-integration-test</id>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,79 @@
/*
* Copyright 2012-2015 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
*
* http://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.test;
import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
import javax.management.ObjectName;
/**
* This sample app simulates the JMX Mbean that is exposed by the Spring
* Boot application.
*/
public class SampleApplication {
private static final Object lock = new Object();
public static void main(String[] args) throws Exception {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ObjectName name = new ObjectName("org.springframework.boot:type=Lifecycle,name=springApplicationLifecycle");
SpringApplicationLifecycle mbean = new SpringApplicationLifecycle();
mbs.registerMBean(mbean, name);
// Flag the app as ready
mbean.ready = true;
int waitAttempts = 0;
while (!mbean.shutdownInvoked) {
if (waitAttempts > 10) {
throw new IllegalStateException("Shutdown should have been invoked by now");
}
synchronized (lock) {
lock.wait(250);
}
waitAttempts++;
}
System.out.println("Application has terminated gracefully");
}
public interface SpringApplicationLifecycleMXBean {
boolean isReady();
void shutdown();
}
static class SpringApplicationLifecycle implements SpringApplicationLifecycleMXBean {
private boolean ready;
private boolean shutdownInvoked;
@Override
public boolean isReady() {
return this.ready;
}
@Override
public void shutdown() {
this.shutdownInvoked = true;
System.out.println("Shutdown requested");
}
}
}

@ -0,0 +1,6 @@
import static org.junit.Assert.assertTrue
def file = new File(basedir, "build.log")
assertTrue 'Shutdown should have been invoked', file.text.contains("Shutdown requested")
assertTrue 'Application should have terminated', file.text.contains("Application has terminated gracefully")

@ -0,0 +1,441 @@
/*
* Copyright 2012-2015 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
*
* http://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.maven;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.filter.collection.AbstractArtifactFeatureFilter;
import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts;
import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.MainClassFinder;
/**
* Base class to run a spring application.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @author David Liu
* @see RunMojo
* @see StartMojo
*/
public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo {
private static final String SPRING_LOADED_AGENT_CLASSNAME = "org.springsource.loaded.agent.SpringLoadedAgent";
/**
* The Maven project.
* @since 1.0
*/
@Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;
/**
* Add maven resources to the classpath directly, this allows live in-place editing of
* resources. Duplicate resources are removed from {@code target/classes} to prevent
* them to appear twice if {@code ClassLoader.getResources()} is called.
* @since 1.0
*/
@Parameter(property = "run.addResources", defaultValue = "true")
private boolean addResources;
/**
* 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")
private File[] agent;
/**
* Flag to say that the agent requires -noverify.
* @since 1.0
*/
@Parameter(property = "run.noverify")
private Boolean noverify;
/**
* JVM arguments that should be associated with the forked process used to run the
* application. On command line, make sure to wrap multiple values between quotes.
* @since 1.1
*/
@Parameter(property = "run.jvmArguments")
private String jvmArguments;
/**
* Arguments that should be passed to the application. On command line use commas to
* separate multiple arguments.
* @since 1.0
*/
@Parameter(property = "run.arguments")
private String[] arguments;
/**
* The name of the main class. If not specified the first compiled class found that
* contains a 'main' method will be used.
* @since 1.0
*/
@Parameter
private String mainClass;
/**
* Additional folders besides the classes directory that should be added to the
* classpath.
* @since 1.0
*/
@Parameter
private String[] folders;
/**
* Directory containing the classes and resource files that should be packaged into
* the archive.
* @since 1.0
*/
@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")
private Boolean fork;
/**
* Specify if the application process should be forked.
* @return {@code true} if the application process should be forked
*/
protected boolean isFork() {
return (Boolean.TRUE.equals(this.fork)
|| (this.fork == null && (hasAgent() || hasJvmArgs())));
}
private boolean hasAgent() {
return (this.agent != null && this.agent.length > 0);
}
private boolean hasJvmArgs() {
return (this.jvmArguments != null && this.jvmArguments.length() > 0);
}
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
final String startClassName = getStartClass();
run(startClassName);
}
private void findAgent() {
try {
if (this.agent == null || this.agent.length == 0) {
Class<?> loaded = Class.forName(SPRING_LOADED_AGENT_CLASSNAME);
if (loaded != null) {
if (this.noverify == null) {
this.noverify = true;
}
CodeSource source = loaded.getProtectionDomain().getCodeSource();
if (source != null) {
this.agent = new File[] {new File(source.getLocation().getFile())};
}
}
}
}
catch (ClassNotFoundException ex) {
// ignore;
}
if (this.noverify == null) {
this.noverify = false;
}
}
private void run(String startClassName) throws MojoExecutionException, MojoFailureException {
findAgent();
if (isFork()) {
doRunWithForkedJvm(startClassName);
}
else {
if (hasAgent()) {
getLog().warn("Fork mode disabled, ignoring agent");
}
if (hasJvmArgs()) {
getLog().warn(
"Fork mode disabled, ignoring JVM argument(s) ["
+ this.jvmArguments + "]");
}
runWithMavenJvm(startClassName, resolveApplicationArguments().asArray());
}
}
private void doRunWithForkedJvm(String startClassName)
throws MojoExecutionException, MojoFailureException {
List<String> args = new ArrayList<String>();
addAgents(args);
addJvmArgs(args);
addClasspath(args);
args.add(startClassName);
addArgs(args);
runWithForkedJvm(args);
}
/**
* Run with a forked VM, using the specified command line arguments.
* @param args the arguments (JVM arguments and application arguments)
* @throws MojoExecutionException
* @throws MojoFailureException
*/
protected abstract void runWithForkedJvm(List<String> args) throws MojoExecutionException, MojoFailureException;
/**
* Run with the current VM, using the specified arguments.
* @param startClassName the class to run
* @param arguments the class arguments
* @throws MojoExecutionException
* @throws MojoFailureException
*/
protected abstract void runWithMavenJvm(String startClassName, String... arguments)
throws MojoExecutionException, MojoFailureException;
/**
* Resolve the application arguments to use.
* @return a {@link RunArguments} defining the application arguments
*/
protected RunArguments resolveApplicationArguments() {
return new RunArguments(this.arguments);
}
private void addArgs(List<String> args) {
RunArguments applicationArguments = resolveApplicationArguments();
Collections.addAll(args, applicationArguments.asArray());
logArguments("Application argument(s): ", this.arguments);
}
/**
* Resolve the JVM arguments to use.
* @return a {@link RunArguments} defining the JVM arguments
*/
protected RunArguments resolveJvmArguments() {
return new RunArguments(this.jvmArguments);
}
private void addJvmArgs(List<String> args) {
RunArguments jvmArguments = resolveJvmArguments();
Collections.addAll(args, jvmArguments.asArray());
logArguments("JVM argument(s): ", jvmArguments.asArray());
}
private void addAgents(List<String> args) {
if (this.agent != null) {
getLog().info("Attaching agents: " + Arrays.asList(this.agent));
for (File agent : this.agent) {
args.add("-javaagent:" + agent);
}
}
if (this.noverify) {
args.add("-noverify");
}
}
private void addClasspath(List<String> args) throws MojoExecutionException {
try {
StringBuilder classpath = new StringBuilder();
for (URL ele : getClassPathUrls()) {
classpath = classpath.append((classpath.length() > 0 ? File.pathSeparator
: "") + new File(ele.toURI()));
}
getLog().debug("Classpath for forked process: " + classpath);
args.add("-cp");
args.add(classpath.toString());
}
catch (Exception e) {
throw new MojoExecutionException("Could not build classpath", e);
}
}
private final String getStartClass() throws MojoExecutionException {
String mainClass = this.mainClass;
if (mainClass == null) {
try {
mainClass = MainClassFinder.findSingleMainClass(this.classesDirectory);
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
}
if (mainClass == null) {
throw new MojoExecutionException("Unable to find a suitable main class, "
+ "please add a 'mainClass' property");
}
return mainClass;
}
protected URL[] getClassPathUrls() throws MojoExecutionException {
try {
List<URL> urls = new ArrayList<URL>();
addUserDefinedFolders(urls);
addResources(urls);
addProjectClasses(urls);
addDependencies(urls);
return urls.toArray(new URL[urls.size()]);
}
catch (MalformedURLException ex) {
throw new MojoExecutionException("Unable to build classpath", ex);
}
catch (IOException ex) {
throw new MojoExecutionException("Unable to build classpath", ex);
}
}
private void addUserDefinedFolders(List<URL> urls) throws MalformedURLException {
if (this.folders != null) {
for (String folder : this.folders) {
urls.add(new File(folder).toURI().toURL());
}
}
}
private void addResources(List<URL> urls) throws IOException {
if (this.addResources) {
for (Resource resource : this.project.getResources()) {
File directory = new File(resource.getDirectory());
urls.add(directory.toURI().toURL());
FileUtils.removeDuplicatesFromOutputDirectory(this.classesDirectory,
directory);
}
}
}
private void addProjectClasses(List<URL> urls) throws MalformedURLException {
urls.add(this.classesDirectory.toURI().toURL());
}
private void addDependencies(List<URL> urls) throws MalformedURLException,
MojoExecutionException {
FilterArtifacts filters = getFilters(new TestArtifactFilter());
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), filters);
for (Artifact artifact : artifacts) {
if (artifact.getFile() != null) {
urls.add(artifact.getFile().toURI().toURL());
}
}
}
private void logArguments(String message, String[] args) {
StringBuffer sb = new StringBuffer(message);
for (String arg : args) {
sb.append(arg).append(" ");
}
getLog().debug(sb.toString().trim());
}
private static class TestArtifactFilter extends AbstractArtifactFeatureFilter {
public TestArtifactFilter() {
super("", Artifact.SCOPE_TEST);
}
@Override
protected String getArtifactFeature(Artifact artifact) {
return artifact.getScope();
}
}
/**
* 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", 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);
}
}
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@ -16,10 +16,13 @@
package org.springframework.boot.maven;
import java.util.Arrays;
import java.util.LinkedList;
import org.codehaus.plexus.util.cli.CommandLineUtils;
/**
* Parse and expose arguments specified as {@link RunMojo} parameters.
* Parse and expose arguments specified in a single string.
*
* @author Stephane Nicoll
* @since 1.1.0
@ -28,13 +31,25 @@ class RunArguments {
private static final String[] NO_ARGS = {};
private final String[] args;
private final LinkedList<String> args;
public RunArguments(String arguments) {
this.args = parseArgs(arguments);
this(parseArgs(arguments));
}
public RunArguments(String[] args) {
this.args = new LinkedList<String>(Arrays.asList(args));
}
private String[] parseArgs(String arguments) {
public LinkedList<String> getArgs() {
return args;
}
public String[] asArray() {
return this.args.toArray(new String[this.args.size()]);
}
private static String[] parseArgs(String arguments) {
if (arguments == null || arguments.trim().isEmpty()) {
return NO_ARGS;
}
@ -48,8 +63,4 @@ class RunArguments {
}
}
public String[] asArray() {
return this.args;
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* Copyright 2012-2015 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.
@ -16,34 +16,16 @@
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;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.shared.artifact.filter.collection.AbstractArtifactFeatureFilter;
import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts;
import org.springframework.boot.loader.tools.FileUtils;
import org.springframework.boot.loader.tools.JavaExecutable;
import org.springframework.boot.loader.tools.MainClassFinder;
import org.springframework.boot.loader.tools.RunProcess;
/**
@ -51,152 +33,15 @@ 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)
public class RunMojo extends AbstractDependencyFilterMojo {
private static final String SPRING_LOADED_AGENT_CLASSNAME = "org.springsource.loaded.agent.SpringLoadedAgent";
/**
* The Maven project.
* @since 1.0
*/
@Parameter(defaultValue = "${project}", readonly = true, required = true)
private MavenProject project;
/**
* Add maven resources to the classpath directly, this allows live in-place editing of
* resources. Duplicate resources are removed from {@code target/classes} to prevent
* them to appear twice if {@code ClassLoader.getResources()} is called.
* @since 1.0
*/
@Parameter(property = "run.addResources", defaultValue = "true")
private boolean addResources;
/**
* 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")
private File[] agent;
/**
* Flag to say that the agent requires -noverify.
* @since 1.0
*/
@Parameter(property = "run.noverify")
private Boolean noverify;
/**
* JVM arguments that should be associated with the forked process used to run the
* application. On command line, make sure to wrap multiple values between quotes.
* @since 1.1
*/
@Parameter(property = "run.jvmArguments")
private String jvmArguments;
/**
* Arguments that should be passed to the application. On command line use commas to
* separate multiple arguments.
* @since 1.0
*/
@Parameter(property = "run.arguments")
private String[] arguments;
/**
* The name of the main class. If not specified the first compiled class found that
* contains a 'main' method will be used.
* @since 1.0
*/
@Parameter
private String mainClass;
/**
* Additional folders besides the classes directory that should be added to the
* classpath.
* @since 1.0
*/
@Parameter
private String[] folders;
/**
* Directory containing the classes and resource files that should be packaged into
* the archive.
* @since 1.0
*/
@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")
private Boolean fork;
public class RunMojo extends AbstractRunMojo {
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
final String startClassName = getStartClass();
run(startClassName);
}
private void findAgent() {
try {
if (this.agent == null || this.agent.length == 0) {
Class<?> loaded = Class.forName(SPRING_LOADED_AGENT_CLASSNAME);
if (loaded != null) {
if (this.noverify == null) {
this.noverify = true;
}
CodeSource source = loaded.getProtectionDomain().getCodeSource();
if (source != null) {
this.agent = new File[] { new File(source.getLocation().getFile()) };
}
}
}
}
catch (ClassNotFoundException ex) {
// ignore;
}
if (this.noverify == null) {
this.noverify = false;
}
}
private void run(String startClassName) throws MojoExecutionException {
findAgent();
boolean hasAgent = (this.agent != null && this.agent.length > 0);
boolean hasJvmArgs = (this.jvmArguments != null && this.jvmArguments.length() > 0);
if (Boolean.TRUE.equals(this.fork)
|| (this.fork == null && (hasAgent || hasJvmArgs))) {
runWithForkedJvm(startClassName);
}
else {
if (hasAgent) {
getLog().warn("Fork mode disabled, ignoring agent");
}
if (hasJvmArgs) {
getLog().warn(
"Fork mode disabled, ignoring JVM argument(s) ["
+ this.jvmArguments + "]");
}
runWithMavenJvm(startClassName);
}
}
private void runWithForkedJvm(String startClassName) throws MojoExecutionException {
List<String> args = new ArrayList<String>();
addAgents(args);
addJvmArgs(args);
addClasspath(args);
args.add(startClassName);
addArgs(args);
protected void runWithForkedJvm(List<String> args) throws MojoExecutionException {
try {
new RunProcess(new JavaExecutable().toString()).run(args
new RunProcess(new JavaExecutable().toString()).run(true, args
.toArray(new String[args.size()]));
}
catch (Exception ex) {
@ -204,131 +49,16 @@ public class RunMojo extends AbstractDependencyFilterMojo {
}
}
private void runWithMavenJvm(String startClassName) throws MojoExecutionException {
protected void runWithMavenJvm(String startClassName, String... arguments) throws MojoExecutionException {
IsolatedThreadGroup threadGroup = new IsolatedThreadGroup(startClassName);
Thread launchThread = new Thread(threadGroup, new LaunchRunner(startClassName,
this.arguments), startClassName + ".main()");
arguments), startClassName + ".main()");
launchThread.setContextClassLoader(new URLClassLoader(getClassPathUrls()));
launchThread.start();
join(threadGroup);
threadGroup.rethrowUncaughtException();
}
private void addAgents(List<String> args) {
if (this.agent != null) {
getLog().info("Attaching agents: " + Arrays.asList(this.agent));
for (File agent : this.agent) {
args.add("-javaagent:" + agent);
}
}
if (this.noverify) {
args.add("-noverify");
}
}
private void addJvmArgs(List<String> args) {
RunArguments jvmArguments = new RunArguments(this.jvmArguments);
Collections.addAll(args, jvmArguments.asArray());
logArguments("JVM argument(s): ", jvmArguments.asArray());
}
private void addClasspath(List<String> args) throws MojoExecutionException {
try {
StringBuilder classpath = new StringBuilder();
for (URL ele : getClassPathUrls()) {
classpath = classpath.append((classpath.length() > 0 ? File.pathSeparator
: "") + new File(ele.toURI()));
}
getLog().debug("Classpath for forked process: " + classpath);
args.add("-cp");
args.add(classpath.toString());
}
catch (Exception e) {
throw new MojoExecutionException("Could not build classpath", e);
}
}
private void addArgs(List<String> args) {
Collections.addAll(args, this.arguments);
logArguments("Application argument(s): ", this.arguments);
}
private final String getStartClass() throws MojoExecutionException {
String mainClass = this.mainClass;
if (mainClass == null) {
try {
mainClass = MainClassFinder.findSingleMainClass(this.classesDirectory);
}
catch (IOException ex) {
throw new MojoExecutionException(ex.getMessage(), ex);
}
}
if (mainClass == null) {
throw new MojoExecutionException("Unable to find a suitable main class, "
+ "please add a 'mainClass' property");
}
return mainClass;
}
private URL[] getClassPathUrls() throws MojoExecutionException {
try {
List<URL> urls = new ArrayList<URL>();
addUserDefinedFolders(urls);
addResources(urls);
addProjectClasses(urls);
addDependencies(urls);
return urls.toArray(new URL[urls.size()]);
}
catch (MalformedURLException ex) {
throw new MojoExecutionException("Unable to build classpath", ex);
}
catch (IOException ex) {
throw new MojoExecutionException("Unable to build classpath", ex);
}
}
private void addUserDefinedFolders(List<URL> urls) throws MalformedURLException {
if (this.folders != null) {
for (String folder : this.folders) {
urls.add(new File(folder).toURI().toURL());
}
}
}
private void addResources(List<URL> urls) throws IOException {
if (this.addResources) {
for (Resource resource : this.project.getResources()) {
File directory = new File(resource.getDirectory());
urls.add(directory.toURI().toURL());
FileUtils.removeDuplicatesFromOutputDirectory(this.classesDirectory,
directory);
}
}
}
private void addProjectClasses(List<URL> urls) throws MalformedURLException {
urls.add(this.classesDirectory.toURI().toURL());
}
private void addDependencies(List<URL> urls) throws MalformedURLException,
MojoExecutionException {
FilterArtifacts filters = getFilters(new TestArtifactFilter());
Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), filters);
for (Artifact artifact : artifacts) {
if (artifact.getFile() != null) {
urls.add(artifact.getFile().toURI().toURL());
}
}
}
private void logArguments(String message, String[] args) {
StringBuffer sb = new StringBuffer(message);
for (String arg : args) {
sb.append(arg).append(" ");
}
getLog().debug(sb.toString().trim());
}
private void join(ThreadGroup threadGroup) {
boolean hasNonDaemonThreads;
do {
@ -350,84 +80,4 @@ public class RunMojo extends AbstractDependencyFilterMojo {
while (hasNonDaemonThreads);
}
private static class TestArtifactFilter extends AbstractArtifactFeatureFilter {
public TestArtifactFilter() {
super("", Artifact.SCOPE_TEST);
}
@Override
protected String getArtifactFeature(Artifact artifact) {
return artifact.getScope();
}
}
/**
* 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);
}
}
}
}

@ -0,0 +1,123 @@
/*
* Copyright 2012-2015 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
*
* http://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.maven;
import java.io.IOException;
import javax.management.AttributeNotFoundException;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanServerConnection;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import org.apache.maven.plugin.MojoExecutionException;
/**
* A JMX client for the {@code SpringApplicationLifecycle} mbean. Permits to obtain
* information about the lifecycle of a given Spring application.
*
* @author Stephane Nicoll
* @since 1.3.0
*/
class SpringApplicationLifecycleClient {
//Note: see org.springframework.boot.autoconfigure.test.SpringApplicationLifecycleAutoConfiguration
static final String DEFAULT_OBJECT_NAME =
"org.springframework.boot:type=Lifecycle,name=springApplicationLifecycle";
private final MBeanServerConnection mBeanServerConnection;
private final ObjectName objectName;
public SpringApplicationLifecycleClient(MBeanServerConnection mBeanServerConnection, String jmxName) {
this.mBeanServerConnection = mBeanServerConnection;
this.objectName = toObjectName(jmxName);
}
/**
* Create a connector for an {@link javax.management.MBeanServer} exposed on the
* current machine and the current port. Security should be disabled.
* @param port the port on which the mbean server is exposed
* @return a connection
* @throws IOException if the connection to that server failed
*/
public static JMXConnector createLocalJmxConnector(int port) throws IOException {
String url = "service:jmx:rmi:///jndi/rmi://127.0.0.1:" + port + "/jmxrmi";
JMXServiceURL serviceUrl = new JMXServiceURL(url);
return JMXConnectorFactory.connect(serviceUrl, null);
}
/**
* Check if the spring application managed by this instance is ready.
* <p>Returns {@code false} if the mbean is not yet deployed so this method
* should be repeatedly called until a timeout is reached.
* @return {@code true} if the application is ready to service requests
* @throws MojoExecutionException if the JMX service could not be contacted
*/
public boolean isReady() throws MojoExecutionException {
try {
return (Boolean) this.mBeanServerConnection.getAttribute(this.objectName, "Ready");
}
catch (InstanceNotFoundException e) {
return false; // Instance not available yet
}
catch (AttributeNotFoundException e) {
throw new IllegalStateException("Unexpected: attribute 'Ready' not available", e);
}
catch (ReflectionException e) {
throw new MojoExecutionException("Failed to retrieve Ready attribute", e.getCause());
}
catch (MBeanException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
catch (IOException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
}
/**
* Stop the application managed by this instance.
* @throws MojoExecutionException if the JMX service could not be contacted
* @throws IOException if an I/O error occurs
* @throws InstanceNotFoundException if the lifecycle mbean cannot be found
*/
public void stop() throws MojoExecutionException, IOException, InstanceNotFoundException {
try {
this.mBeanServerConnection.invoke(this.objectName, "shutdown", null, null);
}
catch (ReflectionException e) {
throw new MojoExecutionException("Shutdown failed", e.getCause());
}
catch (MBeanException e) {
throw new MojoExecutionException("Could not invoke shutdown operation", e);
}
}
private ObjectName toObjectName(String name) {
try {
return new ObjectName(name);
}
catch (MalformedObjectNameException ex) {
throw new IllegalArgumentException("Invalid jmx name '" + name + "'");
}
}
}

@ -0,0 +1,287 @@
/*
* Copyright 2012-2015 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
*
* http://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.maven;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.ConnectException;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import javax.management.MBeanServerConnection;
import javax.management.ReflectionException;
import javax.management.remote.JMXConnector;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.springframework.boot.loader.tools.JavaExecutable;
import org.springframework.boot.loader.tools.RunProcess;
/**
* Start a spring application. Contrary to the {@code run} goal, this does not
* block and allows other goal to operate on the application. This goal is typically
* used in integration test scenario where the application is started before a test
* suite and stopped after.
*
* @author Stephane Nicoll
* @since 1.3.0
* @see StopMojo
*/
@Mojo(name = "start", requiresProject = true, defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, requiresDependencyResolution = ResolutionScope.TEST)
public class StartMojo extends AbstractRunMojo {
private static final String ENABLE_MBEAN_PROPERTY = "--spring.context.lifecycle.enabled=true";
private static final String JMX_NAME_PROPERTY_PREFIX = "--spring.context.lifecycle.jmx-name=";
/**
* The JMX name of the automatically deployed MBean managing the lifecycle
* of the spring application.
*/
@Parameter
private String jmxName = SpringApplicationLifecycleClient.DEFAULT_OBJECT_NAME;
/**
* The port to use to expose the platform MBeanServer if the application
* needs to be forked.
*/
@Parameter
private int jmxPort = 9001;
/**
* The number of milli-seconds to wait between each attempt to check if the
* spring application is ready.
*/
@Parameter
private long wait = 500;
/**
* The maximum number of attempts to check if the spring application is
* ready. Combined with the "wait" argument, this gives a global timeout
* value (30 sec by default)
*/
@Parameter
private int maxAttempts = 60;
private final Object lock = new Object();
@Override
protected void runWithForkedJvm(List<String> args) throws MojoExecutionException, MojoFailureException {
RunProcess runProcess;
try {
runProcess = new RunProcess(new JavaExecutable().toString());
runProcess.run(false, args.toArray(new String[args.size()]));
}
catch (Exception ex) {
throw new MojoExecutionException("Could not exec java", ex);
}
try {
waitForSpringApplication();
}
catch (MojoExecutionException e) {
runProcess.kill();
throw e;
}
catch (MojoFailureException e) {
runProcess.kill();
throw e;
}
}
@Override
protected RunArguments resolveApplicationArguments() {
RunArguments applicationArguments = super.resolveApplicationArguments();
applicationArguments.getArgs().addLast(ENABLE_MBEAN_PROPERTY);
if (isFork()) {
applicationArguments.getArgs().addLast(JMX_NAME_PROPERTY_PREFIX + this.jmxName);
}
return applicationArguments;
}
@Override
protected RunArguments resolveJvmArguments() {
RunArguments jvmArguments = super.resolveJvmArguments();
if (isFork()) {
List<String> remoteJmxArguments = new ArrayList<String>();
remoteJmxArguments.add("-Dcom.sun.management.jmxremote");
remoteJmxArguments.add("-Dcom.sun.management.jmxremote.port=" + jmxPort);
remoteJmxArguments.add("-Dcom.sun.management.jmxremote.authenticate=false");
remoteJmxArguments.add("-Dcom.sun.management.jmxremote.ssl=false");
jvmArguments.getArgs().addAll(remoteJmxArguments);
}
return jvmArguments;
}
protected void runWithMavenJvm(String startClassName, String... arguments) throws MojoExecutionException {
IsolatedThreadGroup threadGroup = new IsolatedThreadGroup(startClassName);
Thread launchThread = new Thread(threadGroup, new LaunchRunner(startClassName,
arguments), startClassName + ".main()");
launchThread.setContextClassLoader(new URLClassLoader(getClassPathUrls()));
launchThread.start();
waitForSpringApplication(this.wait, this.maxAttempts);
}
private void waitForSpringApplication(long wait, int maxAttempts) throws MojoExecutionException {
SpringApplicationLifecycleClient helper = new SpringApplicationLifecycleClient(
ManagementFactory.getPlatformMBeanServer(), this.jmxName);
getLog().debug("Waiting for spring application to start...");
for (int i = 0; i < maxAttempts; i++) {
if (helper.isReady()) {
return;
}
getLog().debug("Spring application is not ready yet, waiting " + wait + "ms (attempt " + (i + 1) + ")");
synchronized (this.lock) {
try {
this.lock.wait(wait);
}
catch (InterruptedException e) {
throw new IllegalStateException("Interrupted while waiting for Spring Boot app to start.");
}
}
}
throw new MojoExecutionException("Spring application did not start before the configured " +
"timeout (" + (wait * maxAttempts) + "ms");
}
private void waitForSpringApplication() throws MojoFailureException, MojoExecutionException {
try {
if (Boolean.TRUE.equals(isFork())) {
waitForForkedSpringApplication();
}
else {
doWaitForSpringApplication(ManagementFactory.getPlatformMBeanServer());
}
}
catch (IOException e) {
throw new MojoFailureException("Could not contact Spring Boot application", e);
}
catch (Exception e) {
throw new MojoExecutionException("Could not figure out if the application has started", e);
}
}
private void waitForForkedSpringApplication() throws IOException, MojoFailureException, MojoExecutionException {
final JMXConnector jmxConnector;
try {
getLog().debug("Connecting to local MBeanServer at port " + this.jmxPort);
jmxConnector = execute(wait, maxAttempts, new RetryCallback<JMXConnector>() {
@Override
public JMXConnector retry() throws Exception {
try {
return SpringApplicationLifecycleClient.createLocalJmxConnector(jmxPort);
}
catch (IOException e) {
if (hasCauseWithType(e, ConnectException.class)) { // Not there yet
getLog().debug("MBean server at port " + jmxPort + " is not up yet...");
return null;
}
else {
throw e;
}
}
}
});
if (jmxConnector == null) {
throw new MojoExecutionException("JMX MBean server was not reachable before the configured " +
"timeout (" + (this.wait * this.maxAttempts) + "ms");
}
getLog().debug("Connected to local MBeanServer at port " + this.jmxPort);
try {
MBeanServerConnection mBeanServerConnection = jmxConnector.getMBeanServerConnection();
doWaitForSpringApplication(mBeanServerConnection);
}
finally {
jmxConnector.close();
}
}
catch (IOException e) {
throw e;
}
catch (Exception e) {
throw new MojoExecutionException("Failed to connect to MBean server at port " + this.jmxPort, e);
}
}
private void doWaitForSpringApplication(MBeanServerConnection connection)
throws IOException, MojoExecutionException, MojoFailureException {
final SpringApplicationLifecycleClient client =
new SpringApplicationLifecycleClient(connection, this.jmxName);
try {
execute(this.wait, this.maxAttempts, new RetryCallback<Boolean>() {
@Override
public Boolean retry() throws Exception {
boolean ready = client.isReady();
// Wait until the app is ready
return (ready ? true : null);
}
});
}
catch (ReflectionException e) {
throw new MojoExecutionException("Unable to retrieve Ready attribute", e.getCause());
}
catch (Exception e) {
throw new MojoFailureException("Could not invoke shutdown operation", e);
}
}
public <T> T execute(long wait, int maxAttempts, RetryCallback<T> callback) throws Exception {
getLog().debug("Waiting for spring application to start...");
for (int i = 0; i < maxAttempts; i++) {
T result = callback.retry();
if (result != null) {
return result;
}
getLog().debug("Spring application is not ready yet, waiting " + wait + "ms (attempt " + (i + 1) + ")");
synchronized (this.lock) {
try {
this.lock.wait(wait);
}
catch (InterruptedException e) {
throw new IllegalStateException("Interrupted while waiting for Spring Boot app to start.");
}
}
}
throw new MojoExecutionException("Spring application did not start before the configured " +
"timeout (" + (wait * maxAttempts) + "ms");
}
private static boolean hasCauseWithType(Throwable t, Class<? extends Exception> type) {
return type.isAssignableFrom(t.getClass()) || t.getCause() != null && hasCauseWithType(t.getCause(), type);
}
interface RetryCallback<T> {
/**
* Attempt to execute an operation. Throws an exception in case of fatal
* exception, returns {@code null} to indicate another attempt should be
* made if possible.
*/
T retry() throws Exception;
}
}

@ -0,0 +1,108 @@
/*
* Copyright 2012-2015 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
*
* http://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.maven;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
/**
* Stop a spring application that has been started by the "start" goal. Typically invoked
* once a test suite has completed.
*
* @author Stephane Nicoll
* @since 1.3.0
*/
@Mojo(name = "stop", requiresProject = true, defaultPhase = LifecyclePhase.POST_INTEGRATION_TEST)
public class StopMojo extends AbstractMojo {
/**
* Flag to indicate if the run processes should be forked. Must be aligned to the value
* used to {@link StartMojo start} the process
* @since 1.2
*/
@Parameter(property = "fork")
private Boolean fork;
/**
* The JMX name of the automatically deployed MBean managing the lifecycle
* of the application.
*/
@Parameter
private String jmxName = SpringApplicationLifecycleClient.DEFAULT_OBJECT_NAME;
/**
* The port to use to lookup the platform MBeanServer if the application
* has been forked.
*/
@Parameter
private int jmxPort = 9001;
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("Stopping application...");
try {
if (Boolean.TRUE.equals(this.fork)) {
stopForkedProcess();
}
else {
stop();
}
}
catch (IOException e) {
// The response won't be received as the server has died - ignoring
getLog().debug("Service is not reachable anymore (" + e.getMessage() + ")");
}
}
private void stop() throws IOException, MojoFailureException, MojoExecutionException {
doStop(ManagementFactory.getPlatformMBeanServer());
}
private void stopForkedProcess() throws IOException, MojoFailureException, MojoExecutionException {
JMXConnector jmxConnector = SpringApplicationLifecycleClient.createLocalJmxConnector(this.jmxPort);
try {
MBeanServerConnection mBeanServerConnection = jmxConnector.getMBeanServerConnection();
doStop(mBeanServerConnection);
}
finally {
jmxConnector.close();
}
}
private void doStop(MBeanServerConnection connection)
throws IOException, MojoExecutionException {
SpringApplicationLifecycleClient helper = new SpringApplicationLifecycleClient(connection, this.jmxName);
try {
helper.stop();
}
catch (InstanceNotFoundException e) {
throw new MojoExecutionException("Spring application lifecycle JMX bean not found (fork is " +
"" + this.fork + "). Could not stop application gracefully", e);
}
}
}

@ -0,0 +1,82 @@
-----
Random port for integration tests
-----
Stephane Nicoll
-----
2015-04-16
-----
One nice feature of the Spring Boot test integration is that it can allocate a free
port for the web application. When the <<<start>>> goal of the plugin is used, the
Spring Boot application is started separately, making it difficult to pass the actual
port to the integration test itself.
The example below showcases how you could achieve the same feature using the
{{{http://mojo.codehaus.org/build-helper-maven-plugin/}build-helper-plugin}}:
---
<project>
...
<build>
...
<plugins>
...
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>reserve-tomcat-port</id>
<goals>
<goal>reserve-network-port</goal>
</goals>
<phase>process-resources</phase>
<configuration>
<portNames>
<portName>tomcat.http.port</portName>
</portNames>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>pre-integration-test</id>
<goals>
<goal>start</goal>
</goals>
<configuration>
<arguments>
<argument>--server.port=${tomcat.http.port}</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>post-integration-test</id>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<test.server.port>${tomcat.http.port}</test.server.port>
</systemPropertyVariables>
</configuration>
</plugin>
...
</plugins>
...
</build>
---
You can now retrieve the <<<test.server.port>>> system property in any of your integration test
to create a proper <<<URL>>> to the server.

@ -19,6 +19,10 @@ Spring Boot Maven Plugin
* {{{./repackage-mojo.html}spring-boot:repackage}} repackages your jar/war to be executable.
* {{{./start-mojo.html}spring-boot:start}} and {{{./stop-mojo.html}spring-boot:stop}} to manage
the lifecycle of your Spring Boot application (i.e. for integration tests).
* Usage
General instructions on how to use the Spring Boot Plugin can be found on the {{{./usage.html}usage page}}. Some
@ -42,6 +46,8 @@ Spring Boot Maven Plugin
* {{{./examples/run-debug.html}Debug the application}}
* {{{./examples/it-random-port.html}Random port for integration tests}}
[]

@ -9,8 +9,19 @@
Usage
The plugin offers a goal for repackaging an application as an executable JAR/WAR as well as a goal
for running the application.
The plugin provides several goals to work with a Spring Boot application:
* <<<repackage>>>: create a jar or war file that is auto-executable. It can replace the regular artifact or can be
attached to the build lifecyle with a separate <<classifier>>.
* <<<run>>>: run your Spring Boot application with several options to pass parameters to it.
* <<<start>>> and <<<stop>>>: integrate your Spring Boot application to the <<<integration-test>>> phase so that
the application starts before it.
[]
Each goal is further described below.
* Repackaging an application
@ -140,3 +151,49 @@ mvn spring-boot:run
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.
* Working with integration tests
While you may start your Spring Boot application very easily from your test (or test suite) itself,
it may be desirable to handle that in the build itself. To make sure that the lifecycle of you Spring
Boot application is properly managed <around> your integration tests, you can use the <<<start>>> and
<<<stop>>> goals as desribed below:
---
<build>
...
<plugins>
...
<plugin>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
<executions>
<execution>
<id>pre-integration-test</id>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>post-integration-test</id>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
...
</plugins>
...
</build>
---
Such setup can now use the {{{http://maven.apache.org/surefire/maven-failsafe-plugin/}failsafe-plugin}} to
run your integration tests as you would expect.
For more detailed examples of how to configure this goal see:
* {{{./examples/it-random-port.html}Random port for integration tests}}
[]

@ -10,6 +10,7 @@
<item name="Custom repackage classifier" href="examples/repackage-classifier.html"/>
<item name="Exclude a dependency" href="examples/exclude-dependency.html"/>
<item name="Debug the application" href="examples/run-debug.html"/>
<item name="Random port for integration tests" href="examples/it-random-port.html"/>
</menu>
<menu ref="reports"/>
</body>

@ -0,0 +1,41 @@
/*
* Copyright 2012-2015 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
*
* http://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.context;
/**
* A simple MBean contract to control the lifecycle of a {@code SpringApplication} via
* JMX. Intended for internal use only.
*
* @author Stephane Nicoll
* @since 1.3.0
*/
public interface SpringApplicationLifecycleMXBean {
/**
* Specify if the application has fully started and is now ready.
* @return {@code true} if the application is ready
* @see org.springframework.boot.context.event.ApplicationReadyEvent
*/
boolean isReady();
/**
* Shutdown the application.
* @see org.springframework.context.ConfigurableApplicationContext#close()
*/
void shutdown();
}

@ -0,0 +1,101 @@
/*
* Copyright 2012-2015 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
*
* http://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.context;
import java.lang.management.ManagementFactory;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.Assert;
/**
* Register a {@link SpringApplicationLifecycleMXBean} implementation to the platform
* {@link MBeanServer}.
*
* @author Stephane Nicoll
* @since 1.3.0
*/
public class SpringApplicationLifecycleRegistrar implements ApplicationContextAware,
InitializingBean, DisposableBean, ApplicationListener<ApplicationReadyEvent> {
private static final Log logger = LogFactory.getLog(SpringApplicationLifecycle.class);
private ConfigurableApplicationContext applicationContext;
private final ObjectName objectName;
private boolean ready = false;
public SpringApplicationLifecycleRegistrar(String name) throws MalformedObjectNameException {
this.objectName = new ObjectName(name);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Assert.isTrue(applicationContext instanceof ConfigurableApplicationContext,
"ApplicationContext does not implement ConfigurableApplicationContext");
this.applicationContext = (ConfigurableApplicationContext) applicationContext;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
ready = true;
}
@Override
public void afterPropertiesSet() throws Exception {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
server.registerMBean(new SpringApplicationLifecycle(), objectName);
if (logger.isDebugEnabled()) {
logger.debug("Application lifecycle MBean registered with name '" + objectName + "'");
}
}
@Override
public void destroy() throws Exception {
ManagementFactory.getPlatformMBeanServer().unregisterMBean(objectName);
}
private class SpringApplicationLifecycle implements SpringApplicationLifecycleMXBean {
@Override
public boolean isReady() {
return ready;
}
@Override
public void shutdown() {
logger.info("Application shutdown requested.");
applicationContext.close();
}
}
}

@ -0,0 +1,142 @@
/*
* Copyright 2012-2015 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
*
* http://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.context;
import java.lang.management.ManagementFactory;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests for {@link SpringApplicationLifecycleRegistrar}.
*
* @author Stephane Nicoll
*/
public class SpringApplicationLifecycleRegistrarTests {
private static final String OBJECT_NAME = "org.springframework.boot:type=Test,name=springApplicationLifecycle";
@Rule
public final ExpectedException thrown = ExpectedException.none();
private MBeanServer mBeanServer;
private ConfigurableApplicationContext context;
@Before
public void setup() throws MalformedObjectNameException {
this.mBeanServer = ManagementFactory.getPlatformMBeanServer();
}
@After
public void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void validateReadyFlag() {
final ObjectName objectName = createObjectName(OBJECT_NAME);
SpringApplication application = new SpringApplication(Config.class);
application.setWebEnvironment(false);
application.addListeners(new ApplicationListener<ContextRefreshedEvent>() {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
try {
assertFalse("Application should not be ready yet", isCurrentApplicationReady(objectName));
}
catch (Exception e) {
throw new IllegalStateException("Could not contact spring application lifecycle bean", e);
}
}
});
this.context = application.run();
assertTrue("application should be ready now", isCurrentApplicationReady(objectName));
}
@Test
public void shutdownApp() throws InstanceNotFoundException {
final ObjectName objectName = createObjectName(OBJECT_NAME);
SpringApplication application = new SpringApplication(Config.class);
application.setWebEnvironment(false);
this.context = application.run();
assertTrue("application should be running", this.context.isRunning());
invokeShutdown(objectName);
assertFalse("application should not be running", this.context.isRunning());
thrown.expect(InstanceNotFoundException.class); // JMX cleanup
this.mBeanServer.getObjectInstance(objectName);
}
private Boolean isCurrentApplicationReady(ObjectName objectName) {
try {
return (Boolean) this.mBeanServer.getAttribute(objectName, "Ready");
}
catch (Exception ex) {
throw new IllegalStateException(ex.getMessage(), ex);
}
}
private void invokeShutdown(ObjectName objectName) {
try {
this.mBeanServer.invoke(objectName, "shutdown", null, null);
}
catch (Exception ex) {
throw new IllegalStateException(ex.getMessage(), ex);
}
}
private ObjectName createObjectName(String jmxName) {
try {
return new ObjectName(jmxName);
}
catch (MalformedObjectNameException e) {
throw new IllegalStateException("Invalid jmx name " + jmxName, e);
}
}
@Configuration
static class Config {
@Bean
public SpringApplicationLifecycleRegistrar springApplicationLifecycle()
throws MalformedObjectNameException {
return new SpringApplicationLifecycleRegistrar(OBJECT_NAME);
}
}
}
Loading…
Cancel
Save