diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/SpringApplicationLifecycleAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/SpringApplicationLifecycleAutoConfiguration.java new file mode 100644 index 0000000000..56e8006239 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/context/SpringApplicationLifecycleAutoConfiguration.java @@ -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); + } + +} diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 059d9715f8..aad5d81dbc 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -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,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/SpringApplicationLifecycleAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/SpringApplicationLifecycleAutoConfigurationTests.java new file mode 100644 index 0000000000..b7242d40c3 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/context/SpringApplicationLifecycleAutoConfigurationTests.java @@ -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; + } + +} + diff --git a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/shell/RunProcessCommand.java b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/shell/RunProcessCommand.java index 9515bb8084..3c11166c80 100644 --- a/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/shell/RunProcessCommand.java +++ b/spring-boot-cli/src/main/java/org/springframework/boot/cli/command/shell/RunProcessCommand.java @@ -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 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; } diff --git a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RunProcess.java b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RunProcess.java index 98f00ef387..27e36a9521 100644 --- a/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RunProcess.java +++ b/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/RunProcess.java @@ -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 args) throws IOException { + /** + * Kill this process. + */ + public void kill() { + doKill(); + } + + protected int run(boolean waitForProcess, Collection 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) { diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/invoker.properties b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/invoker.properties new file mode 100644 index 0000000000..793d89fcd3 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/invoker.properties @@ -0,0 +1 @@ +invoker.goals=clean verify \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/pom.xml b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/pom.xml new file mode 100644 index 0000000000..ec30683987 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + org.springframework.boot.maven.it + start-stop-fork + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + true + + + + + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/src/main/java/org/test/SampleApplication.java b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/src/main/java/org/test/SampleApplication.java new file mode 100644 index 0000000000..1f97f2d0b3 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/src/main/java/org/test/SampleApplication.java @@ -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"); + } + } +} \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/verify.groovy b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/verify.groovy new file mode 100644 index 0000000000..c987b04030 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop-fork/verify.groovy @@ -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") + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/invoker.properties b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/invoker.properties new file mode 100644 index 0000000000..793d89fcd3 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/invoker.properties @@ -0,0 +1 @@ +invoker.goals=clean verify \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/pom.xml b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/pom.xml new file mode 100644 index 0000000000..bc00975554 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + org.springframework.boot.maven.it + start-stop + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + + + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/src/main/java/org/test/SampleApplication.java b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/src/main/java/org/test/SampleApplication.java new file mode 100644 index 0000000000..1f97f2d0b3 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/src/main/java/org/test/SampleApplication.java @@ -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"); + } + } +} \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/verify.groovy b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/verify.groovy new file mode 100644 index 0000000000..c987b04030 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/it/start-stop/verify.groovy @@ -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") + diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java new file mode 100644 index 0000000000..563a5ddec1 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -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 args = new ArrayList(); + 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 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 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 args) { + RunArguments jvmArguments = resolveJvmArguments(); + Collections.addAll(args, jvmArguments.asArray()); + logArguments("JVM argument(s): ", jvmArguments.asArray()); + } + + private void addAgents(List 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 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 urls = new ArrayList(); + 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 urls) throws MalformedURLException { + if (this.folders != null) { + for (String folder : this.folders) { + urls.add(new File(folder).toURI().toURL()); + } + } + } + + private void addResources(List 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 urls) throws MalformedURLException { + urls.add(this.classesDirectory.toURI().toURL()); + } + + private void addDependencies(List urls) throws MalformedURLException, + MojoExecutionException { + FilterArtifacts filters = getFilters(new TestArtifactFilter()); + Set 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); + } + } + + } + +} diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunArguments.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunArguments.java index dfcfe97149..599e676b4f 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunArguments.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunArguments.java @@ -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 args; public RunArguments(String arguments) { - this.args = parseArgs(arguments); + this(parseArgs(arguments)); } - private String[] parseArgs(String arguments) { + public RunArguments(String[] args) { + this.args = new LinkedList(Arrays.asList(args)); + } + + public LinkedList 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; - } - } diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java index 684b962894..c15385fa17 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java @@ -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() { + protected void runWithForkedJvm(List args) throws MojoExecutionException { 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 args = new ArrayList(); - addAgents(args); - addJvmArgs(args); - addClasspath(args); - args.add(startClassName); - addArgs(args); - 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 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 args) { - RunArguments jvmArguments = new RunArguments(this.jvmArguments); - Collections.addAll(args, jvmArguments.asArray()); - logArguments("JVM argument(s): ", jvmArguments.asArray()); - } - - private void addClasspath(List 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 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 urls = new ArrayList(); - 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 urls) throws MalformedURLException { - if (this.folders != null) { - for (String folder : this.folders) { - urls.add(new File(folder).toURI().toURL()); - } - } - } - - private void addResources(List 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 urls) throws MalformedURLException { - urls.add(this.classesDirectory.toURI().toURL()); - } - - private void addDependencies(List urls) throws MalformedURLException, - MojoExecutionException { - FilterArtifacts filters = getFilters(new TestArtifactFilter()); - Set 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); - } - } - - } - } diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringApplicationLifecycleClient.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringApplicationLifecycleClient.java new file mode 100644 index 0000000000..df26ad9c2a --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringApplicationLifecycleClient.java @@ -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. + *

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 + "'"); + } + } + +} diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java new file mode 100644 index 0000000000..129af92691 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java @@ -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 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 remoteJmxArguments = new ArrayList(); + 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() { + @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() { + @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 execute(long wait, int maxAttempts, RetryCallback 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 type) { + return type.isAssignableFrom(t.getClass()) || t.getCause() != null && hasCauseWithType(t.getCause(), type); + } + + + interface RetryCallback { + + /** + * 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; + } + +} diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StopMojo.java b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StopMojo.java new file mode 100644 index 0000000000..e103551d59 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StopMojo.java @@ -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); + } + } + +} diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/it-random-port.apt.vm b/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/it-random-port.apt.vm new file mode 100644 index 0000000000..2acd949568 --- /dev/null +++ b/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/examples/it-random-port.apt.vm @@ -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 <<>> 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}}: + +--- + + ... + + ... + + ... + + org.codehaus.mojo + build-helper-maven-plugin + + + reserve-tomcat-port + + reserve-network-port + + process-resources + + + tomcat.http.port + + + + + + + ${project.groupId} + ${project.artifactId} + ${project.version} + + + pre-integration-test + + start + + + + --server.port=${tomcat.http.port} + + + + + post-integration-test + + stop + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + ${tomcat.http.port} + + + + ... + + ... + +--- + + You can now retrieve the <<>> system property in any of your integration test + to create a proper <<>> to the server. diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/index.apt b/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/index.apt index 515b38b41a..1c041ec32e 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/index.apt +++ b/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/index.apt @@ -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}} + [] diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm b/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm index f4e983e67e..9d252b44c2 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm +++ b/spring-boot-tools/spring-boot-maven-plugin/src/site/apt/usage.apt.vm @@ -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: + + * <<>>: 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 <>. + + * <<>>: run your Spring Boot application with several options to pass parameters to it. + + * <<>> and <<>>: integrate your Spring Boot application to the <<>> 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 your integration tests, you can use the <<>> and + <<>> goals as desribed below: + +--- + + ... + + ... + + ${project.groupId} + ${project.artifactId} + ${project.version} + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + ... + + ... + +--- + + 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}} + + [] diff --git a/spring-boot-tools/spring-boot-maven-plugin/src/site/site.xml b/spring-boot-tools/spring-boot-maven-plugin/src/site/site.xml index e72b40577c..b8c6591f21 100644 --- a/spring-boot-tools/spring-boot-maven-plugin/src/site/site.xml +++ b/spring-boot-tools/spring-boot-maven-plugin/src/site/site.xml @@ -10,6 +10,7 @@ +

diff --git a/spring-boot/src/main/java/org/springframework/boot/context/SpringApplicationLifecycleMXBean.java b/spring-boot/src/main/java/org/springframework/boot/context/SpringApplicationLifecycleMXBean.java new file mode 100644 index 0000000000..6511f25af6 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/SpringApplicationLifecycleMXBean.java @@ -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(); + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/SpringApplicationLifecycleRegistrar.java b/spring-boot/src/main/java/org/springframework/boot/context/SpringApplicationLifecycleRegistrar.java new file mode 100644 index 0000000000..e76ad0fcb0 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/SpringApplicationLifecycleRegistrar.java @@ -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 { + + 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(); + } + } + +} + diff --git a/spring-boot/src/test/java/org/springframework/boot/context/SpringApplicationLifecycleRegistrarTests.java b/spring-boot/src/test/java/org/springframework/boot/context/SpringApplicationLifecycleRegistrarTests.java new file mode 100644 index 0000000000..7b06862e54 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/context/SpringApplicationLifecycleRegistrarTests.java @@ -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() { + @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); + } + } + +}