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.reflect.Method;
022import java.net.MalformedURLException;
023import java.net.URL;
024import java.security.CodeSource;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collections;
028import java.util.List;
029import java.util.Set;
030
031import org.apache.maven.artifact.Artifact;
032import org.apache.maven.model.Resource;
033import org.apache.maven.plugin.MojoExecutionException;
034import org.apache.maven.plugin.MojoFailureException;
035import org.apache.maven.plugins.annotations.Parameter;
036import org.apache.maven.project.MavenProject;
037import org.apache.maven.shared.artifact.filter.collection.AbstractArtifactFeatureFilter;
038import org.apache.maven.shared.artifact.filter.collection.FilterArtifacts;
039
040import org.springframework.boot.loader.tools.FileUtils;
041import org.springframework.boot.loader.tools.MainClassFinder;
042
043/**
044 * Base class to run a spring application.
045 *
046 * @author Phillip Webb
047 * @author Stephane Nicoll
048 * @author David Liu
049 * @author Daniel Young
050 * @see RunMojo
051 * @see StartMojo
052 */
053public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo {
054
055        private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";
056
057        private static final String SPRING_LOADED_AGENT_CLASS_NAME = "org.springsource.loaded.agent.SpringLoadedAgent";
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 = "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 = "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 = "run.noverify")
090        private Boolean noverify;
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 = "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 = "run.jvmArguments")
109        private String jvmArguments;
110
111        /**
112         * Arguments that should be passed to the application. On command line use commas to
113         * separate multiple arguments.
114         * @since 1.0
115         */
116        @Parameter(property = "run.arguments")
117        private String[] arguments;
118
119        /**
120         * The spring profiles to activate. Convenience shortcut of specifying the
121         * 'spring.profiles.active' argument. On command line use commas to separate multiple
122         * profiles.
123         * @since 1.3
124         */
125        @Parameter(property = "run.profiles")
126        private String[] profiles;
127
128        /**
129         * The name of the main class. If not specified the first compiled class found that
130         * contains a 'main' method will be used.
131         * @since 1.0
132         */
133        @Parameter
134        private String mainClass;
135
136        /**
137         * Additional folders besides the classes directory that should be added to the
138         * classpath.
139         * @since 1.0
140         */
141        @Parameter
142        private String[] folders;
143
144        /**
145         * Directory containing the classes and resource files that should be packaged into
146         * the archive.
147         * @since 1.0
148         */
149        @Parameter(defaultValue = "${project.build.outputDirectory}", required = true)
150        private File classesDirectory;
151
152        /**
153         * Flag to indicate if the run processes should be forked. {@code fork} is
154         * automatically enabled if an agent, jvmArguments or working directory are specified,
155         * or if devtools is present.
156         * @since 1.2
157         */
158        @Parameter(property = "fork")
159        private Boolean fork;
160
161        /**
162         * Flag to include the test classpath when running.
163         * @since 1.3
164         */
165        @Parameter(property = "useTestClasspath", defaultValue = "false")
166        private Boolean useTestClasspath;
167
168        /**
169         * Skip the execution.
170         * @since 1.3.2
171         */
172        @Parameter(property = "skip", defaultValue = "false")
173        private boolean skip;
174
175        @Override
176        public void execute() throws MojoExecutionException, MojoFailureException {
177                if (this.skip) {
178                        getLog().debug("skipping run as per configuration.");
179                        return;
180                }
181                run(getStartClass());
182        }
183
184        /**
185         * Specify if the application process should be forked.
186         * @return {@code true} if the application process should be forked
187         */
188        protected boolean isFork() {
189                return (Boolean.TRUE.equals(this.fork)
190                                || (this.fork == null && enableForkByDefault()));
191        }
192
193        /**
194         * Specify if fork should be enabled by default.
195         * @return {@code true} if fork should be enabled by default
196         * @see #logDisabledFork()
197         */
198        protected boolean enableForkByDefault() {
199                return hasAgent() || hasJvmArgs() || hasWorkingDirectorySet();
200        }
201
202        private boolean hasAgent() {
203                return (this.agent != null && this.agent.length > 0);
204        }
205
206        private boolean hasJvmArgs() {
207                return (this.jvmArguments != null && this.jvmArguments.length() > 0);
208        }
209
210        private boolean hasWorkingDirectorySet() {
211                return this.workingDirectory != null;
212        }
213
214        private void findAgent() {
215                try {
216                        if (this.agent == null || this.agent.length == 0) {
217                                Class<?> loaded = Class.forName(SPRING_LOADED_AGENT_CLASS_NAME);
218                                if (loaded != null) {
219                                        if (this.noverify == null) {
220                                                this.noverify = true;
221                                        }
222                                        CodeSource source = loaded.getProtectionDomain().getCodeSource();
223                                        if (source != null) {
224                                                this.agent = new File[] {
225                                                                new File(source.getLocation().getFile()) };
226                                        }
227                                }
228                        }
229                }
230                catch (ClassNotFoundException ex) {
231                        // ignore;
232                }
233                if (this.noverify == null) {
234                        this.noverify = false;
235                }
236        }
237
238        private void run(String startClassName)
239                        throws MojoExecutionException, MojoFailureException {
240                findAgent();
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 (hasAgent()) {
260                        getLog().warn("Fork mode disabled, ignoring agent");
261                }
262                if (hasJvmArgs()) {
263                        getLog().warn("Fork mode disabled, ignoring JVM argument(s) ["
264                                        + this.jvmArguments + "]");
265                }
266                if (hasWorkingDirectorySet()) {
267                        getLog().warn("Fork mode disabled, ignoring working directory configuration");
268                }
269        }
270
271        private void doRunWithForkedJvm(String startClassName)
272                        throws MojoExecutionException, MojoFailureException {
273                List<String> args = new ArrayList<String>();
274                addAgents(args);
275                addJvmArgs(args);
276                addClasspath(args);
277                args.add(startClassName);
278                addArgs(args);
279                runWithForkedJvm(this.workingDirectory, args);
280        }
281
282        /**
283         * Run with a forked VM, using the specified command line arguments.
284         * @param workingDirectory the working directory of the forked JVM
285         * @param args the arguments (JVM arguments and application arguments)
286         * @throws MojoExecutionException in case of MOJO execution errors
287         * @throws MojoFailureException in case of MOJO failures
288         */
289        protected abstract void runWithForkedJvm(File workingDirectory, List<String> args)
290                        throws MojoExecutionException, MojoFailureException;
291
292        /**
293         * Run with the current VM, using the specified arguments.
294         * @param startClassName the class to run
295         * @param arguments the class arguments
296         * @throws MojoExecutionException in case of MOJO execution errors
297         * @throws MojoFailureException in case of MOJO failures
298         */
299        protected abstract void runWithMavenJvm(String startClassName, String... arguments)
300                        throws MojoExecutionException, MojoFailureException;
301
302        /**
303         * Resolve the application arguments to use.
304         * @return a {@link RunArguments} defining the application arguments
305         */
306        protected RunArguments resolveApplicationArguments() {
307                RunArguments runArguments = new RunArguments(this.arguments);
308                addActiveProfileArgument(runArguments);
309                return runArguments;
310        }
311
312        private void addArgs(List<String> args) {
313                RunArguments applicationArguments = resolveApplicationArguments();
314                Collections.addAll(args, applicationArguments.asArray());
315                logArguments("Application argument(s): ", this.arguments);
316        }
317
318        /**
319         * Resolve the JVM arguments to use.
320         * @return a {@link RunArguments} defining the JVM arguments
321         */
322        protected RunArguments resolveJvmArguments() {
323                return new RunArguments(this.jvmArguments);
324        }
325
326        private void addJvmArgs(List<String> args) {
327                RunArguments jvmArguments = resolveJvmArguments();
328                Collections.addAll(args, jvmArguments.asArray());
329                logArguments("JVM argument(s): ", jvmArguments.asArray());
330        }
331
332        private void addAgents(List<String> args) {
333                if (this.agent != null) {
334                        getLog().info("Attaching agents: " + Arrays.asList(this.agent));
335                        for (File agent : this.agent) {
336                                args.add("-javaagent:" + agent);
337                        }
338                }
339                if (this.noverify) {
340                        args.add("-noverify");
341                }
342        }
343
344        private void addActiveProfileArgument(RunArguments arguments) {
345                if (this.profiles.length > 0) {
346                        StringBuilder arg = new StringBuilder("--spring.profiles.active=");
347                        for (int i = 0; i < this.profiles.length; i++) {
348                                arg.append(this.profiles[i]);
349                                if (i < this.profiles.length - 1) {
350                                        arg.append(",");
351                                }
352                        }
353                        arguments.getArgs().addFirst(arg.toString());
354                        logArguments("Active profile(s): ", this.profiles);
355                }
356        }
357
358        private void addClasspath(List<String> args) throws MojoExecutionException {
359                try {
360                        StringBuilder classpath = new StringBuilder();
361                        for (URL ele : getClassPathUrls()) {
362                                classpath = classpath
363                                                .append((classpath.length() > 0 ? File.pathSeparator : "")
364                                                                + new File(ele.toURI()));
365                        }
366                        getLog().debug("Classpath for forked process: " + classpath);
367                        args.add("-cp");
368                        args.add(classpath.toString());
369                }
370                catch (Exception ex) {
371                        throw new MojoExecutionException("Could not build classpath", ex);
372                }
373        }
374
375        private String getStartClass() throws MojoExecutionException {
376                String mainClass = this.mainClass;
377                if (mainClass == null) {
378                        try {
379                                mainClass = MainClassFinder.findSingleMainClass(this.classesDirectory,
380                                                SPRING_BOOT_APPLICATION_CLASS_NAME);
381                        }
382                        catch (IOException ex) {
383                                throw new MojoExecutionException(ex.getMessage(), ex);
384                        }
385                }
386                if (mainClass == null) {
387                        throw new MojoExecutionException("Unable to find a suitable main class, "
388                                        + "please add a 'mainClass' property");
389                }
390                return mainClass;
391        }
392
393        protected URL[] getClassPathUrls() throws MojoExecutionException {
394                try {
395                        List<URL> urls = new ArrayList<URL>();
396                        addUserDefinedFolders(urls);
397                        addResources(urls);
398                        addProjectClasses(urls);
399                        addDependencies(urls);
400                        return urls.toArray(new URL[urls.size()]);
401                }
402                catch (MalformedURLException ex) {
403                        throw new MojoExecutionException("Unable to build classpath", ex);
404                }
405                catch (IOException ex) {
406                        throw new MojoExecutionException("Unable to build classpath", ex);
407                }
408        }
409
410        private void addUserDefinedFolders(List<URL> urls) throws MalformedURLException {
411                if (this.folders != null) {
412                        for (String folder : this.folders) {
413                                urls.add(new File(folder).toURI().toURL());
414                        }
415                }
416        }
417
418        private void addResources(List<URL> urls) throws IOException {
419                if (this.addResources) {
420                        for (Resource resource : this.project.getResources()) {
421                                File directory = new File(resource.getDirectory());
422                                urls.add(directory.toURI().toURL());
423                                FileUtils.removeDuplicatesFromOutputDirectory(this.classesDirectory,
424                                                directory);
425                        }
426                }
427        }
428
429        private void addProjectClasses(List<URL> urls) throws MalformedURLException {
430                urls.add(this.classesDirectory.toURI().toURL());
431        }
432
433        private void addDependencies(List<URL> urls)
434                        throws MalformedURLException, MojoExecutionException {
435                FilterArtifacts filters = this.useTestClasspath ? getFilters()
436                                : getFilters(new TestArtifactFilter());
437                Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
438                                filters);
439                for (Artifact artifact : artifacts) {
440                        if (artifact.getFile() != null) {
441                                urls.add(artifact.getFile().toURI().toURL());
442                        }
443                }
444        }
445
446        private void logArguments(String message, String[] args) {
447                StringBuilder sb = new StringBuilder(message);
448                for (String arg : args) {
449                        sb.append(arg).append(" ");
450                }
451                getLog().debug(sb.toString().trim());
452        }
453
454        private static class TestArtifactFilter extends AbstractArtifactFeatureFilter {
455
456                TestArtifactFilter() {
457                        super("", Artifact.SCOPE_TEST);
458                }
459
460                @Override
461                protected String getArtifactFeature(Artifact artifact) {
462                        return artifact.getScope();
463                }
464
465        }
466
467        /**
468         * Isolated {@link ThreadGroup} to capture uncaught exceptions.
469         */
470        class IsolatedThreadGroup extends ThreadGroup {
471
472                private final Object monitor = new Object();
473
474                private Throwable exception;
475
476                IsolatedThreadGroup(String name) {
477                        super(name);
478                }
479
480                @Override
481                public void uncaughtException(Thread thread, Throwable ex) {
482                        if (!(ex instanceof ThreadDeath)) {
483                                synchronized (this.monitor) {
484                                        this.exception = (this.exception == null ? ex : this.exception);
485                                }
486                                getLog().warn(ex);
487                        }
488                }
489
490                public void rethrowUncaughtException() throws MojoExecutionException {
491                        synchronized (this.monitor) {
492                                if (this.exception != null) {
493                                        throw new MojoExecutionException(
494                                                        "An exception occurred while running. "
495                                                                        + this.exception.getMessage(),
496                                                        this.exception);
497                                }
498                        }
499                }
500
501        }
502
503        /**
504         * Runner used to launch the application.
505         */
506        class LaunchRunner implements Runnable {
507
508                private final String startClassName;
509
510                private final String[] args;
511
512                LaunchRunner(String startClassName, String... args) {
513                        this.startClassName = startClassName;
514                        this.args = (args != null ? args : new String[] {});
515                }
516
517                @Override
518                public void run() {
519                        Thread thread = Thread.currentThread();
520                        ClassLoader classLoader = thread.getContextClassLoader();
521                        try {
522                                Class<?> startClass = classLoader.loadClass(this.startClassName);
523                                Method mainMethod = startClass.getMethod("main", String[].class);
524                                if (!mainMethod.isAccessible()) {
525                                        mainMethod.setAccessible(true);
526                                }
527                                mainMethod.invoke(null, new Object[] { this.args });
528                        }
529                        catch (NoSuchMethodException ex) {
530                                Exception wrappedEx = new Exception(
531                                                "The specified mainClass doesn't contain a "
532                                                                + "main method with appropriate signature.",
533                                                ex);
534                                thread.getThreadGroup().uncaughtException(thread, wrappedEx);
535                        }
536                        catch (Exception ex) {
537                                thread.getThreadGroup().uncaughtException(thread, ex);
538                        }
539                }
540
541        }
542
543}