Skip to content

Commit

Permalink
Add start/stop goals to maven plugin
Browse files Browse the repository at this point in the history
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 spring-projectsgh-2525
  • Loading branch information
snicoll committed May 19, 2015
1 parent 129c249 commit e0dfe9f
Show file tree
Hide file tree
Showing 26 changed files with 1,893 additions and 382 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,\
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}

Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -48,7 +48,7 @@ public ExitStatus run(String... args) throws Exception {

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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -31,6 +31,7 @@
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
* @author Stephane Nicoll
* @since 1.1.0
*/
public class RunProcess {
Expand All @@ -50,11 +51,18 @@ public RunProcess(String... command) {
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);
Expand All @@ -71,17 +79,22 @@ public void run() {
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;
}
}
}

Expand Down Expand Up @@ -163,7 +176,11 @@ public boolean handleSigInt() {
if (hasJustEnded()) {
return true;
}
return doKill();

}

private boolean doKill() {
// destroy the running process
Process process = this.process;
if (process != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invoker.goals=clean verify
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit e0dfe9f

Please sign in to comment.