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}