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