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}