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}