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-2525pull/3013/head
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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
invoker.goals=clean verify
|
@ -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 @@
|
||||
invoker.goals=clean verify
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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.
|
@ -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…
Reference in New Issue