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.reflect.Method;
022import java.net.MalformedURLException;
023import java.net.URL;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import java.util.stream.Collectors;
031
032import org.apache.maven.artifact.Artifact;
033import org.apache.maven.model.Resource;
034import org.apache.maven.plugin.MojoExecutionException;
035import org.apache.maven.plugin.MojoFailureException;
036import org.apache.maven.plugins.annotations.Parameter;
037import org.apache.maven.project.MavenProject;
038import org.apache.maven.shared.artifact.filter.collection.AbstractArtifactFeatureFilter;
039import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts;
040
041import org.springframework.boot.loader.tools.FileUtils;
042import org.springframework.boot.loader.tools.MainClassFinder;
043
044/**
045 * Base class to run a spring application.
046 *
047 * @author Phillip Webb
048 * @author Stephane Nicoll
049 * @author David Liu
050 * @author Daniel Young
051 * @author Dmytro Nosan
052 * @see RunMojo
053 * @see StartMojo
054 */
055public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo {
056
057        private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
058
059        /**
060         * The Maven project.
061         * @since 1.0
062         */
063        @Parameter(defaultValue = "${project}", readonly = true, required = true)
064        private MavenProject project;
065
066        /**
067         * Add maven resources to the classpath directly, this allows live in-place editing of
068         * resources. Duplicate resources are removed from {@code target/classes} to prevent
069         * them to appear twice if {@code ClassLoader.getResources()} is called. Please
070         * consider adding {@code spring-boot-devtools} to your project instead as it provides
071         * this feature and many more.
072         * @since 1.0
073         */
074        @Parameter(property = "spring-boot.run.addResources", defaultValue = "false")
075        private boolean addResources = false;
076
077        /**
078         * Path to agent jar. NOTE: the use of agents means that processes will be started by
079         * forking a new JVM.
080         * @since 1.0
081         */
082        @Parameter(property = "spring-boot.run.agent")
083        private File[] agent;
084
085        /**
086         * Flag to say that the agent requires -noverify.
087         * @since 1.0
088         */
089        @Parameter(property = "spring-boot.run.noverify")
090        private boolean noverify = false;
091
092        /**
093         * Current working directory to use for the application. If not specified, basedir
094         * will be used. NOTE: the use of working directory means that processes will be
095         * started by forking a new JVM.
096         * @since 1.5
097         */
098        @Parameter(property = "spring-boot.run.workingDirectory")
099        private File workingDirectory;
100
101        /**
102         * JVM arguments that should be associated with the forked process used to run the
103         * application. On command line, make sure to wrap multiple values between quotes.
104         * NOTE: the use of JVM arguments means that processes will be started by forking a
105         * new JVM.
106         * @since 1.1
107         */
108        @Parameter(property = "spring-boot.run.jvmArguments")
109        private String jvmArguments;
110
111        /**
112         * List of JVM system properties to pass to the process. NOTE: the use of system
113         * properties means that processes will be started by forking a new JVM.
114         * @since 2.1
115         */
116        @Parameter
117        private Map<String, String> systemPropertyVariables;
118
119        /**
120         * List of Environment variables that should be associated with the forked process
121         * used to run the application. NOTE: the use of Environment variables means that
122         * processes will be started by forking a new JVM.
123         * @since 2.1
124         */
125        @Parameter
126        private Map<String, String> environmentVariables;
127
128        /**
129         * Arguments that should be passed to the application. On command line use commas to
130         * separate multiple arguments.
131         * @since 1.0
132         */
133        @Parameter(property = "spring-boot.run.arguments")
134        private String[] arguments;
135
136        /**
137         * The spring profiles to activate. Convenience shortcut of specifying the
138         * 'spring.profiles.active' argument. On command line use commas to separate multiple
139         * profiles.
140         * @since 1.3
141         */
142        @Parameter(property = "spring-boot.run.profiles")
143        private String[] profiles;
144
145        /**
146         * The name of the main class. If not specified the first compiled class found that
147         * contains a 'main' method will be used.
148         * @since 1.0
149         */
150        @Parameter(property = "spring-boot.run.main-class")
151        private String mainClass;
152
153        /**
154         * Additional folders besides the classes directory that should be added to the
155         * classpath.
156         * @since 1.0
157         */
158        @Parameter(property = "spring-boot.run.folders")
159        private String[] folders;
160
161        /**
162         * Directory containing the classes and resource files that should be packaged into
163         * the archive.
164         * @since 1.0
165         */
166        @Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
167        private File classesDirectory;
168
169        /**
170         * Flag to indicate if the run processes should be forked. {@code fork} is
171         * automatically enabled if an agent, jvmArguments or working directory are specified,
172         * or if devtools is present.
173         * @since 1.2
174         */
175        @Parameter(property = "spring-boot.run.fork")
176        private Boolean fork;
177
178        /**
179         * Flag to include the test classpath when running.
180         * @since 1.3
181         */
182        @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false")
183        private Boolean useTestClasspath;
184
185        /**
186         * Skip the execution.
187         * @since 1.3.2
188         */
189        @Parameter(property = "spring-boot.run.skip", defaultValue = "false")
190        private boolean skip;
191
192        @Override
193        public void execute() throws MojoExecutionException, MojoFailureException {
194                if (this.skip) {
195                        getLog().debug("skipping run as per configuration.");
196                        return;
197                }
198                run(getStartClass());
199        }
200
201        /**
202         * Specify if the application process should be forked.
203         * @return {@code true} if the application process should be forked
204         */
205        protected boolean isFork() {
206                return (Boolean.TRUE.equals(this.fork)
207                                || (this.fork == null && enableForkByDefault()));
208        }
209
210        /**
211         * Specify if fork should be enabled by default.
212         * @return {@code true} if fork should be enabled by default
213         * @see #logDisabledFork()
214         */
215        protected boolean enableForkByDefault() {
216                return hasAgent() || hasJvmArgs() || hasEnvVariables()
217                                || hasWorkingDirectorySet();
218        }
219
220        private boolean hasAgent() {
221                return (this.agent != null && this.agent.length > 0);
222        }
223
224        private boolean hasJvmArgs() {
225                return (this.jvmArguments != null && !this.jvmArguments.isEmpty())
226                                || (this.systemPropertyVariables != null
227                                                && !this.systemPropertyVariables.isEmpty());
228        }
229
230        private boolean hasEnvVariables() {
231                return (this.environmentVariables != null
232                                && !this.environmentVariables.isEmpty());
233        }
234
235        private boolean hasWorkingDirectorySet() {
236                return this.workingDirectory != null;
237        }
238
239        private void run(String startClassName)
240                        throws MojoExecutionException, MojoFailureException {
241                boolean fork = isFork();
242                this.project.getProperties().setProperty("_spring.boot.fork.enabled",
243                                Boolean.toString(fork));
244                if (fork) {
245                        doRunWithForkedJvm(startClassName);
246                }
247                else {
248                        logDisabledFork();
249                        runWithMavenJvm(startClassName, resolveApplicationArguments().asArray());
250                }
251        }
252
253        /**
254         * Log a warning indicating that fork mode has been explicitly disabled while some
255         * conditions are present that require to enable it.
256         * @see #enableForkByDefault()
257         */
258        protected void logDisabledFork() {
259                if (getLog().isWarnEnabled()) {
260                        if (hasAgent()) {
261                                getLog().warn("Fork mode disabled, ignoring agent");
262                        }
263                        if (hasJvmArgs()) {
264                                RunArguments runArguments = resolveJvmArguments();
265                                getLog().warn("Fork mode disabled, ignoring JVM argument(s) [" + Arrays
266                                                .stream(runArguments.asArray()).collect(Collectors.joining(" "))
267                                                + "]");
268                        }
269                        if (hasWorkingDirectorySet()) {
270                                getLog().warn(
271                                                "Fork mode disabled, ignoring working directory configuration");
272                        }
273                }
274        }
275
276        private void doRunWithForkedJvm(String startClassName)
277                        throws MojoExecutionException, MojoFailureException {
278                List<String> args = new ArrayList<>();
279                addAgents(args);
280                addJvmArgs(args);
281                addClasspath(args);
282                args.add(startClassName);
283                addArgs(args);
284                runWithForkedJvm(this.workingDirectory, args, determineEnvironmentVariables());
285        }
286
287        /**
288         * Run with a forked VM, using the specified command line arguments.
289         * @param workingDirectory the working directory of the forked JVM
290         * @param args the arguments (JVM arguments and application arguments)
291         * @param environmentVariables the environment variables
292         * @throws MojoExecutionException in case of MOJO execution errors
293         * @throws MojoFailureException in case of MOJO failures
294         */
295        protected abstract void runWithForkedJvm(File workingDirectory, List<String> args,
296                        Map<String, String> environmentVariables)
297                        throws MojoExecutionException, MojoFailureException;
298
299        /**
300         * Run with the current VM, using the specified arguments.
301         * @param startClassName the class to run
302         * @param arguments the class arguments
303         * @throws MojoExecutionException in case of MOJO execution errors
304         * @throws MojoFailureException in case of MOJO failures
305         */
306        protected abstract void runWithMavenJvm(String startClassName, String... arguments)
307                        throws MojoExecutionException, MojoFailureException;
308
309        /**
310         * Resolve the application arguments to use.
311         * @return a {@link RunArguments} defining the application arguments
312         */
313        protected RunArguments resolveApplicationArguments() {
314                RunArguments runArguments = new RunArguments(this.arguments);
315                addActiveProfileArgument(runArguments);
316                return runArguments;
317        }
318
319        /**
320         * Resolve the environment variables to use.
321         * @return an {@link EnvVariables} defining the environment variables
322         */
323        protected EnvVariables resolveEnvVariables() {
324                return new EnvVariables(this.environmentVariables);
325        }
326
327        private void addArgs(List<String> args) {
328                RunArguments applicationArguments = resolveApplicationArguments();
329                Collections.addAll(args, applicationArguments.asArray());
330                logArguments("Application argument(s): ", this.arguments);
331        }
332
333        private Map<String, String> determineEnvironmentVariables() {
334                EnvVariables envVariables = resolveEnvVariables();
335                logArguments("Environment variable(s): ", envVariables.asArray());
336                return envVariables.asMap();
337        }
338
339        /**
340         * Resolve the JVM arguments to use.
341         * @return a {@link RunArguments} defining the JVM arguments
342         */
343        protected RunArguments resolveJvmArguments() {
344                StringBuilder stringBuilder = new StringBuilder();
345                if (this.systemPropertyVariables != null) {
346                        stringBuilder.append(this.systemPropertyVariables.entrySet().stream()
347                                        .map((e) -> SystemPropertyFormatter.format(e.getKey(), e.getValue()))
348                                        .collect(Collectors.joining(" ")));
349                }
350                if (this.jvmArguments != null) {
351                        stringBuilder.append(" ").append(this.jvmArguments);
352                }
353                return new RunArguments(stringBuilder.toString());
354        }
355
356        private void addJvmArgs(List<String> args) {
357                RunArguments jvmArguments = resolveJvmArguments();
358                Collections.addAll(args, jvmArguments.asArray());
359                logArguments("JVM argument(s): ", jvmArguments.asArray());
360        }
361
362        private void addAgents(List<String> args) {
363                if (this.agent != null) {
364                        if (getLog().isInfoEnabled()) {
365                                getLog().info("Attaching agents: " + Arrays.asList(this.agent));
366                        }
367                        for (File agent : this.agent) {
368                                args.add("-javaagent:" + agent);
369                        }
370                }
371                if (this.noverify) {
372                        args.add("-noverify");
373                }
374        }
375
376        private void addActiveProfileArgument(RunArguments arguments) {
377                if (this.profiles.length > 0) {
378                        StringBuilder arg = new StringBuilder("--spring.profiles.active=");
379                        for (int i = 0; i < this.profiles.length; i++) {
380                                arg.append(this.profiles[i]);
381                                if (i < this.profiles.length - 1) {
382                                        arg.append(",");
383                                }
384                        }
385                        arguments.getArgs().addFirst(arg.toString());
386                        logArguments("Active profile(s): ", this.profiles);
387                }
388        }
389
390        private void addClasspath(List<String> args) throws MojoExecutionException {
391                try {
392                        StringBuilder classpath = new StringBuilder();
393                        for (URL ele : getClassPathUrls()) {
394                                if (classpath.length() > 0) {
395                                        classpath.append(File.pathSeparator);
396                                }
397                                classpath.append(new File(ele.toURI()));
398                        }
399                        if (getLog().isDebugEnabled()) {
400                                getLog().debug("Classpath for forked process: " + classpath);
401                        }
402                        args.add("-cp");
403                        args.add(classpath.toString());
404                }
405                catch (Exception ex) {
406                        throw new MojoExecutionException("Could not build classpath", ex);
407                }
408        }
409
410        private String getStartClass() throws MojoExecutionException {
411                String mainClass = this.mainClass;
412                if (mainClass == null) {
413                        try {
414                                mainClass = MainClassFinder.findSingleMainClass(this.classesDirectory,
415                                                SPRING_BOOT_APPLICATION_CLASS_NAME);
416                        }
417                        catch (IOException ex) {
418                                throw new MojoExecutionException(ex.getMessage(), ex);
419                        }
420                }
421                if (mainClass == null) {
422                        throw new MojoExecutionException("Unable to find a suitable main class, "
423                                        + "please add a 'mainClass' property");
424                }
425                return mainClass;
426        }
427
428        protected URL[] getClassPathUrls() throws MojoExecutionException {
429                try {
430                        List<URL> urls = new ArrayList<>();
431                        addUserDefinedFolders(urls);
432                        addResources(urls);
433                        addProjectClasses(urls);
434                        addDependencies(urls);
435                        return urls.toArray(new URL[0]);
436                }
437                catch (IOException ex) {
438                        throw new MojoExecutionException("Unable to build classpath", ex);
439                }
440        }
441
442        private void addUserDefinedFolders(List<URL> urls) throws MalformedURLException {
443                if (this.folders != null) {
444                        for (String folder : this.folders) {
445                                urls.add(new File(folder).toURI().toURL());
446                        }
447                }
448        }
449
450        private void addResources(List<URL> urls) throws IOException {
451                if (this.addResources) {
452                        for (Resource resource : this.project.getResources()) {
453                                File directory = new File(resource.getDirectory());
454                                urls.add(directory.toURI().toURL());
455                                FileUtils.removeDuplicatesFromOutputDirectory(this.classesDirectory,
456                                                directory);
457                        }
458                }
459        }
460
461        private void addProjectClasses(List<URL> urls) throws MalformedURLException {
462                urls.add(this.classesDirectory.toURI().toURL());
463        }
464
465        private void addDependencies(List<URL> urls)
466                        throws MalformedURLException, MojoExecutionException {
467                FilterArtifacts filters = (this.useTestClasspath ? getFilters()
468                                : getFilters(new TestArtifactFilter()));
469                Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
470                                filters);
471                for (Artifact artifact : artifacts) {
472                        if (artifact.getFile() != null) {
473                                urls.add(artifact.getFile().toURI().toURL());
474                        }
475                }
476        }
477
478        private void logArguments(String message, String[] args) {
479                if (getLog().isDebugEnabled()) {
480                        getLog().debug(
481                                        Arrays.stream(args).collect(Collectors.joining(" ", message, "")));
482                }
483        }
484
485        private static class TestArtifactFilter extends AbstractArtifactFeatureFilter {
486
487                TestArtifactFilter() {
488                        super("", Artifact.SCOPE_TEST);
489                }
490
491                @Override
492                protected String getArtifactFeature(Artifact artifact) {
493                        return artifact.getScope();
494                }
495
496        }
497
498        /**
499         * Isolated {@link ThreadGroup} to capture uncaught exceptions.
500         */
501        class IsolatedThreadGroup extends ThreadGroup {
502
503                private final Object monitor = new Object();
504
505                private Throwable exception;
506
507                IsolatedThreadGroup(String name) {
508                        super(name);
509                }
510
511                @Override
512                public void uncaughtException(Thread thread, Throwable ex) {
513                        if (!(ex instanceof ThreadDeath)) {
514                                synchronized (this.monitor) {
515                                        this.exception = (this.exception != null) ? this.exception : ex;
516                                }
517                                getLog().warn(ex);
518                        }
519                }
520
521                public void rethrowUncaughtException() throws MojoExecutionException {
522                        synchronized (this.monitor) {
523                                if (this.exception != null) {
524                                        throw new MojoExecutionException(
525                                                        "An exception occurred while running. "
526                                                                        + this.exception.getMessage(),
527                                                        this.exception);
528                                }
529                        }
530                }
531
532        }
533
534        /**
535         * Runner used to launch the application.
536         */
537        class LaunchRunner implements Runnable {
538
539                private final String startClassName;
540
541                private final String[] args;
542
543                LaunchRunner(String startClassName, String... args) {
544                        this.startClassName = startClassName;
545                        this.args = (args != null) ? args : new String[] {};
546                }
547
548                @Override
549                public void run() {
550                        Thread thread = Thread.currentThread();
551                        ClassLoader classLoader = thread.getContextClassLoader();
552                        try {
553                                Class<?> startClass = classLoader.loadClass(this.startClassName);
554                                Method mainMethod = startClass.getMethod("main", String[].class);
555                                if (!mainMethod.isAccessible()) {
556                                        mainMethod.setAccessible(true);
557                                }
558                                mainMethod.invoke(null, new Object[] { this.args });
559                        }
560                        catch (NoSuchMethodException ex) {
561                                Exception wrappedEx = new Exception(
562                                                "The specified mainClass doesn't contain a "
563                                                                + "main method with appropriate signature.",
564                                                ex);
565                                thread.getThreadGroup().uncaughtException(thread, wrappedEx);
566                        }
567                        catch (Exception ex) {
568                                thread.getThreadGroup().uncaughtException(thread, ex);
569                        }
570                }
571
572        }
573
574        /**
575         * Format System properties.
576         */
577        static class SystemPropertyFormatter {
578
579                public static String format(String key, String value) {
580                        if (key == null) {
581                                return "";
582                        }
583                        if (value == null || value.isEmpty()) {
584                                return String.format("-D%s", key);
585                        }
586                        return String.format("-D%s=\"%s\"", key, value);
587                }
588
589        }
590
591}