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}