001/* 002 * Copyright 2012-2016 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.maven; 018 019import java.io.File; 020import java.io.IOException; 021import java.lang.management.ManagementFactory; 022import java.net.ConnectException; 023import java.net.URLClassLoader; 024import java.util.ArrayList; 025import java.util.List; 026import java.util.concurrent.Callable; 027 028import javax.management.MBeanServerConnection; 029import javax.management.ReflectionException; 030import javax.management.remote.JMXConnector; 031 032import org.apache.maven.plugin.MojoExecutionException; 033import org.apache.maven.plugin.MojoFailureException; 034import org.apache.maven.plugins.annotations.LifecyclePhase; 035import org.apache.maven.plugins.annotations.Mojo; 036import org.apache.maven.plugins.annotations.Parameter; 037import org.apache.maven.plugins.annotations.ResolutionScope; 038 039import org.springframework.boot.loader.tools.JavaExecutable; 040import org.springframework.boot.loader.tools.RunProcess; 041 042/** 043 * Start a spring application. Contrary to the {@code run} goal, this does not block and 044 * allows other goal to operate on the application. This goal is typically used in 045 * integration test scenario where the application is started before a test suite and 046 * stopped after. 047 * 048 * @author Stephane Nicoll 049 * @since 1.3.0 050 * @see StopMojo 051 */ 052@Mojo(name = "start", requiresProject = true, defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST, requiresDependencyResolution = ResolutionScope.TEST) 053public class StartMojo extends AbstractRunMojo { 054 055 private static final String ENABLE_MBEAN_PROPERTY = "--spring.application.admin.enabled=true"; 056 057 private static final String JMX_NAME_PROPERTY_PREFIX = "--spring.application.admin.jmx-name="; 058 059 /** 060 * The JMX name of the automatically deployed MBean managing the lifecycle of the 061 * spring application. 062 */ 063 @Parameter 064 private String jmxName = SpringApplicationAdminClient.DEFAULT_OBJECT_NAME; 065 066 /** 067 * The port to use to expose the platform MBeanServer if the application needs to be 068 * forked. 069 */ 070 @Parameter 071 private int jmxPort = 9001; 072 073 /** 074 * The number of milli-seconds to wait between each attempt to check if the spring 075 * application is ready. 076 */ 077 @Parameter 078 private long wait = 500; 079 080 /** 081 * The maximum number of attempts to check if the spring application is ready. 082 * Combined with the "wait" argument, this gives a global timeout value (30 sec by 083 * default) 084 */ 085 @Parameter 086 private int maxAttempts = 60; 087 088 private final Object lock = new Object(); 089 090 @Override 091 protected void runWithForkedJvm(File workingDirectory, List<String> args) 092 throws MojoExecutionException, MojoFailureException { 093 RunProcess runProcess = runProcess(workingDirectory, args); 094 try { 095 waitForSpringApplication(); 096 } 097 catch (MojoExecutionException ex) { 098 runProcess.kill(); 099 throw ex; 100 } 101 catch (MojoFailureException ex) { 102 runProcess.kill(); 103 throw ex; 104 } 105 } 106 107 private RunProcess runProcess(File workingDirectory, List<String> args) 108 throws MojoExecutionException { 109 try { 110 RunProcess runProcess = new RunProcess(workingDirectory, 111 new JavaExecutable().toString()); 112 runProcess.run(false, args.toArray(new String[args.size()])); 113 return runProcess; 114 } 115 catch (Exception ex) { 116 throw new MojoExecutionException("Could not exec java", ex); 117 } 118 } 119 120 @Override 121 protected RunArguments resolveApplicationArguments() { 122 RunArguments applicationArguments = super.resolveApplicationArguments(); 123 applicationArguments.getArgs().addLast(ENABLE_MBEAN_PROPERTY); 124 if (isFork()) { 125 applicationArguments.getArgs() 126 .addLast(JMX_NAME_PROPERTY_PREFIX + this.jmxName); 127 } 128 return applicationArguments; 129 } 130 131 @Override 132 protected RunArguments resolveJvmArguments() { 133 RunArguments jvmArguments = super.resolveJvmArguments(); 134 if (isFork()) { 135 List<String> remoteJmxArguments = new ArrayList<String>(); 136 remoteJmxArguments.add("-Dcom.sun.management.jmxremote"); 137 remoteJmxArguments.add("-Dcom.sun.management.jmxremote.port=" + this.jmxPort); 138 remoteJmxArguments.add("-Dcom.sun.management.jmxremote.authenticate=false"); 139 remoteJmxArguments.add("-Dcom.sun.management.jmxremote.ssl=false"); 140 jvmArguments.getArgs().addAll(remoteJmxArguments); 141 } 142 return jvmArguments; 143 } 144 145 @Override 146 protected void runWithMavenJvm(String startClassName, String... arguments) 147 throws MojoExecutionException { 148 IsolatedThreadGroup threadGroup = new IsolatedThreadGroup(startClassName); 149 Thread launchThread = new Thread(threadGroup, 150 new LaunchRunner(startClassName, arguments), startClassName + ".main()"); 151 launchThread.setContextClassLoader(new URLClassLoader(getClassPathUrls())); 152 launchThread.start(); 153 waitForSpringApplication(this.wait, this.maxAttempts); 154 } 155 156 private void waitForSpringApplication(long wait, int maxAttempts) 157 throws MojoExecutionException { 158 SpringApplicationAdminClient client = new SpringApplicationAdminClient( 159 ManagementFactory.getPlatformMBeanServer(), this.jmxName); 160 getLog().debug("Waiting for spring application to start..."); 161 for (int i = 0; i < maxAttempts; i++) { 162 if (client.isReady()) { 163 return; 164 } 165 String message = "Spring application is not ready yet, waiting " + wait 166 + "ms (attempt " + (i + 1) + ")"; 167 getLog().debug(message); 168 synchronized (this.lock) { 169 try { 170 this.lock.wait(wait); 171 } 172 catch (InterruptedException ex) { 173 Thread.currentThread().interrupt(); 174 throw new IllegalStateException( 175 "Interrupted while waiting for Spring Boot app to start."); 176 } 177 } 178 } 179 throw new MojoExecutionException( 180 "Spring application did not start before the configured timeout (" 181 + (wait * maxAttempts) + "ms"); 182 } 183 184 private void waitForSpringApplication() 185 throws MojoFailureException, MojoExecutionException { 186 try { 187 if (isFork()) { 188 waitForForkedSpringApplication(); 189 } 190 else { 191 doWaitForSpringApplication(ManagementFactory.getPlatformMBeanServer()); 192 } 193 } 194 catch (IOException ex) { 195 throw new MojoFailureException("Could not contact Spring Boot application", 196 ex); 197 } 198 catch (Exception ex) { 199 throw new MojoExecutionException( 200 "Could not figure out if the application has started", ex); 201 } 202 } 203 204 private void waitForForkedSpringApplication() 205 throws IOException, MojoFailureException, MojoExecutionException { 206 try { 207 getLog().debug("Connecting to local MBeanServer at port " + this.jmxPort); 208 JMXConnector connector = execute(this.wait, this.maxAttempts, 209 new CreateJmxConnector(this.jmxPort)); 210 if (connector == null) { 211 throw new MojoExecutionException( 212 "JMX MBean server was not reachable before the configured " 213 + "timeout (" + (this.wait * this.maxAttempts) + "ms"); 214 } 215 getLog().debug("Connected to local MBeanServer at port " + this.jmxPort); 216 try { 217 MBeanServerConnection connection = connector.getMBeanServerConnection(); 218 doWaitForSpringApplication(connection); 219 } 220 finally { 221 connector.close(); 222 } 223 } 224 catch (IOException ex) { 225 throw ex; 226 } 227 catch (Exception ex) { 228 throw new MojoExecutionException( 229 "Failed to connect to MBean server at port " + this.jmxPort, ex); 230 } 231 } 232 233 private void doWaitForSpringApplication(MBeanServerConnection connection) 234 throws IOException, MojoExecutionException, MojoFailureException { 235 final SpringApplicationAdminClient client = new SpringApplicationAdminClient( 236 connection, this.jmxName); 237 try { 238 execute(this.wait, this.maxAttempts, new Callable<Boolean>() { 239 240 @Override 241 public Boolean call() throws Exception { 242 return (client.isReady() ? true : null); 243 } 244 245 }); 246 } 247 catch (ReflectionException ex) { 248 throw new MojoExecutionException("Unable to retrieve 'ready' attribute", 249 ex.getCause()); 250 } 251 catch (Exception ex) { 252 throw new MojoFailureException("Could not invoke shutdown operation", ex); 253 } 254 } 255 256 /** 257 * Execute a task, retrying it on failure. 258 * @param <T> the result type 259 * @param wait the wait time 260 * @param maxAttempts the maximum number of attempts 261 * @param callback the task to execute (possibly multiple times). The callback should 262 * return {@code null} to indicate that another attempt should be made 263 * @return the result 264 * @throws Exception in case of execution errors 265 */ 266 public <T> T execute(long wait, int maxAttempts, Callable<T> callback) 267 throws Exception { 268 getLog().debug("Waiting for spring application to start..."); 269 for (int i = 0; i < maxAttempts; i++) { 270 T result = callback.call(); 271 if (result != null) { 272 return result; 273 } 274 String message = "Spring application is not ready yet, waiting " + wait 275 + "ms (attempt " + (i + 1) + ")"; 276 getLog().debug(message); 277 synchronized (this.lock) { 278 try { 279 this.lock.wait(wait); 280 } 281 catch (InterruptedException ex) { 282 Thread.currentThread().interrupt(); 283 throw new IllegalStateException( 284 "Interrupted while waiting for Spring Boot app to start."); 285 } 286 } 287 } 288 throw new MojoExecutionException( 289 "Spring application did not start before the configured " + "timeout (" 290 + (wait * maxAttempts) + "ms"); 291 } 292 293 private class CreateJmxConnector implements Callable<JMXConnector> { 294 295 private final int port; 296 297 CreateJmxConnector(int port) { 298 this.port = port; 299 } 300 301 @Override 302 public JMXConnector call() throws Exception { 303 try { 304 return SpringApplicationAdminClient.connect(this.port); 305 } 306 catch (IOException ex) { 307 if (hasCauseWithType(ex, ConnectException.class)) { 308 String message = "MBean server at port " + this.port 309 + " is not up yet..."; 310 getLog().debug(message); 311 return null; 312 } 313 throw ex; 314 } 315 } 316 317 private boolean hasCauseWithType(Throwable t, Class<? extends Exception> type) { 318 return type.isAssignableFrom(t.getClass()) 319 || t.getCause() != null && hasCauseWithType(t.getCause(), type); 320 } 321 322 } 323 324}