diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/running.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/running.adoc index b50ebd0d87..a1f877c626 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/running.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/running.adoc @@ -43,7 +43,7 @@ You can restore it at any time by configuring your project: include::../maven/running/hot-refresh-pom.xml[tags=hot-refresh] ---- -When `addResources` is enabled, any `src/main/resources` directory will be added to the application classpath when you run the application and any duplicate found in `target/classes` will be removed. +When `addResources` is enabled, any `src/main/resources` directory will be added to the application classpath when you run the application and any duplicate found in the classes output will be removed. This allows hot refreshing of resources which can be very useful when developing web applications. For example, you can work on HTML, CSS or JavaScript files and see your changes immediately without recompiling your application. It is also a helpful way of allowing your front end developers to work without needing to download and install a Java IDE. @@ -53,14 +53,14 @@ NOTE: A side effect of using this feature is that filtering of resources at buil In order to be consistent with the `repackage` goal, the `run` goal builds the classpath in such a way that any dependency that is excluded in the plugin's configuration gets excluded from the classpath as well. For more details, see <>. -Sometimes it is useful to include test dependencies when running the application. -For example, if you want to run your application in a test mode that uses stub classes. -If you wish to do this, you can set the `useTestClasspath` parameter to true. - -NOTE: This is only applied when you run an application: the `repackage` goal will not add test dependencies to the resulting JAR/WAR. +Sometimes it is useful to run a test variant of your application. +For example, if you want to {spring-boot-reference}/#features.testing.testcontainers.at-development-time[use Testcontainers at development time] or make use of some test stubs. +Use the `test-run` goal with many of the same features and configuration options as `run` for this purpose. include::goals/run.adoc[leveloffset=+1] +include::goals/test-run.adoc[leveloffset=+1] + [[run.examples]] @@ -70,7 +70,7 @@ include::goals/run.adoc[leveloffset=+1] [[run.examples.debug]] === Debug the Application -The `run` goal runs your application in a forked process. +The `run` and `test-run` goals run your application in a forked process. If you need to debug it, you should add the necessary JVM arguments to enable remote debugging. The following configuration suspend the process until a debugger has joined on port 5005: diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/TestRunIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/TestRunIntegrationTests.java new file mode 100644 index 0000000000..c0ca4e4750 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/TestRunIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.io.IOException; + +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.contentOf; + +/** + * Integration tests for the Maven plugin's {@code test-run} goal. + * + * @author Andy Wilkinson + */ +@ExtendWith(MavenBuildExtension.class) +class TestRunIntegrationTests { + + @TestTemplate + void whenTheTestRunGoalIsExecutedTheApplicationIsRunWithTestAndMainClassesAndTestClasspath(MavenBuild mavenBuild) { + mavenBuild.project("test-run") + .goals("spring-boot:test-run", "-X") + .execute((project) -> assertThat(buildLog(project)) + .contains("Main class name = org.test.TestSampleApplication") + .contains("1. " + canonicalPathOf(project, "target/test-classes")) + .contains("2. " + canonicalPathOf(project, "target/classes")) + .containsPattern("3\\. .*spring-core") + .containsPattern("4\\. .*spring-jcl")); + } + + private String canonicalPathOf(File project, String path) throws IOException { + return new File(project, path).getCanonicalPath(); + } + + private String buildLog(File project) { + return contentOf(new File(project, "target/build.log")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/test-run/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/test-run/pom.xml new file mode 100644 index 0000000000..3ea7f6d096 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/test-run/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + org.springframework.boot.maven.it + test-run + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + + + org.springframework + spring-core + @spring-framework.version@ + test + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/test-run/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/test-run/src/main/java/org/test/SampleApplication.java new file mode 100644 index 0000000000..94e5520be8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/test-run/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +public class SampleApplication { + + public static void main(String[] args) { + System.out.println("I haz been run"); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/test-run/src/test/java/org/test/TestSampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/test-run/src/test/java/org/test/TestSampleApplication.java new file mode 100644 index 0000000000..b729f9c56e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/test-run/src/test/java/org/test/TestSampleApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.test; + +import java.io.File; +import java.lang.management.ManagementFactory; + +public class TestSampleApplication { + + public static void main(String[] args) { + System.out.println("Main class name = " + TestSampleApplication.class.getName()); + int i = 1; + for (String entry : ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) { + System.out.println(i++ + ". " + entry); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index 8cff9304f6..c2c6a91477 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -173,20 +173,13 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { private String[] directories; /** - * Directory containing the classes and resource files that should be packaged into - * the archive. + * Directory containing the classes and resource files that should be used to run the + * application. * @since 1.0.0 */ @Parameter(defaultValue = "${project.build.outputDirectory}", required = true) private File classesDirectory; - /** - * Flag to include the test classpath when running. - * @since 1.3.0 - */ - @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") - private Boolean useTestClasspath; - /** * Skip the execution. * @since 1.3.2 @@ -200,11 +193,29 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { getLog().debug("skipping run as per configuration."); return; } - String startClass = (this.mainClass != null) ? this.mainClass - : SpringBootApplicationClassFinder.findSingleClass(this.classesDirectory); - run(startClass); + run(determineMainClass()); } + private String determineMainClass() throws MojoExecutionException { + if (this.mainClass != null) { + return this.mainClass; + } + return SpringBootApplicationClassFinder.findSingleClass(getClassesDirectories()); + } + + /** + * Returns the directories that contain the application's classes and resources. When + * the application's main class has not been configured, each directory is searched in + * turn for an appropriate main class. + * @return the directories that contain the application's classes and resources + * @since 3.1.0 + */ + protected List getClassesDirectories() { + return List.of(this.classesDirectory); + } + + protected abstract boolean isUseTestClasspath(); + private void run(String startClassName) throws MojoExecutionException, MojoFailureException { List args = new ArrayList<>(); addAgents(args); @@ -361,17 +372,21 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { for (Resource resource : this.project.getResources()) { File directory = new File(resource.getDirectory()); urls.add(directory.toURI().toURL()); - FileUtils.removeDuplicatesFromOutputDirectory(this.classesDirectory, directory); + for (File classesDirectory : getClassesDirectories()) { + FileUtils.removeDuplicatesFromOutputDirectory(classesDirectory, directory); + } } } } private void addProjectClasses(List urls) throws MalformedURLException { - urls.add(this.classesDirectory.toURI().toURL()); + for (File classesDirectory : getClassesDirectories()) { + urls.add(classesDirectory.toURI().toURL()); + } } private void addDependencies(List urls) throws MalformedURLException, MojoExecutionException { - Set artifacts = (this.useTestClasspath) ? filterDependencies(this.project.getArtifacts()) + Set artifacts = (isUseTestClasspath()) ? filterDependencies(this.project.getArtifacts()) : filterDependencies(this.project.getArtifacts(), new ExcludeTestScopeArtifactFilter()); for (Artifact artifact : artifacts) { if (artifact.getFile() != null) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java index 6c0579c721..fc9b9c4403 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java @@ -51,6 +51,13 @@ public class RunMojo extends AbstractRunMojo { @Parameter(property = "spring-boot.run.optimizedLaunch", defaultValue = "true") private boolean optimizedLaunch; + /** + * Flag to include the test classpath when running. + * @since 1.3.0 + */ + @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") + private Boolean useTestClasspath; + @Override protected RunArguments resolveJvmArguments() { RunArguments jvmArguments = super.resolveJvmArguments(); @@ -69,6 +76,11 @@ public class RunMojo extends AbstractRunMojo { .run(workingDirectory, args, environmentVariables); } + @Override + protected boolean isUseTestClasspath() { + return this.useTestClasspath; + } + private static final class RunProcessKiller implements Runnable { private final RunProcess runProcess; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringBootApplicationClassFinder.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringBootApplicationClassFinder.java index efa4757c28..03040e609d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringBootApplicationClassFinder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/SpringBootApplicationClassFinder.java @@ -18,6 +18,7 @@ package org.springframework.boot.maven; import java.io.File; import java.io.IOException; +import java.util.List; import org.apache.maven.plugin.MojoExecutionException; @@ -34,11 +35,17 @@ abstract class SpringBootApplicationClassFinder { private static final String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication"; static String findSingleClass(File classesDirectory) throws MojoExecutionException { + return findSingleClass(List.of(classesDirectory)); + } + + static String findSingleClass(List classesDirectories) throws MojoExecutionException { try { - String mainClass = MainClassFinder.findSingleMainClass(classesDirectory, - SPRING_BOOT_APPLICATION_CLASS_NAME); - if (mainClass != null) { - return mainClass; + for (File classesDirectory : classesDirectories) { + String mainClass = MainClassFinder.findSingleMainClass(classesDirectory, + SPRING_BOOT_APPLICATION_CLASS_NAME); + if (mainClass != null) { + return mainClass; + } } throw new MojoExecutionException("Unable to find a suitable main class, please add a 'mainClass' property"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java index 6ea9b65aca..d7fda1688c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java @@ -85,6 +85,12 @@ public class StartMojo extends AbstractRunMojo { private final Object lock = new Object(); + /** + * Flag to include the test classpath when running. + */ + @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") + private Boolean useTestClasspath; + @Override protected void run(JavaProcessExecutor processExecutor, File workingDirectory, List args, Map environmentVariables) throws MojoExecutionException, MojoFailureException { @@ -188,6 +194,11 @@ public class StartMojo extends AbstractRunMojo { "Spring application did not start before the configured timeout (" + (wait * maxAttempts) + "ms"); } + @Override + protected boolean isUseTestClasspath() { + return this.useTestClasspath; + } + private class CreateJmxConnector implements Callable { private final int port; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java new file mode 100644 index 0000000000..4e961c0230 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.maven; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Execute; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +import org.springframework.boot.loader.tools.RunProcess; + +/** + * Run an application in place using the test runtime classpath. The main class that will + * be used to launch the application is determined as follows: + * + *
    + *
  1. The configured main class, if any
  2. + *
  3. The main class found in the test classes directory, if any
  4. + *
  5. The main class found in the classes directory, if any
  6. + *
+ * + * @author Phillip Webb + * @author Dmytro Nosan + * @author Stephane Nicoll + * @author Andy Wilkinson + * @since 3.1.0 + */ +@Mojo(name = "test-run", requiresProject = true, defaultPhase = LifecyclePhase.VALIDATE, + requiresDependencyResolution = ResolutionScope.TEST) +@Execute(phase = LifecyclePhase.TEST_COMPILE) +public class TestRunMojo extends AbstractRunMojo { + + /** + * Whether the JVM's launch should be optimized. + */ + @Parameter(property = "spring-boot.test-run.optimizedLaunch", defaultValue = "true") + private boolean optimizedLaunch; + + /** + * Directory containing the test classes and resource files that should be used to run + * the application. + */ + @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) + private File testClassesDirectory; + + @Override + protected List getClassesDirectories() { + ArrayList classesDirectories = new ArrayList<>(super.getClassesDirectories()); + classesDirectories.add(0, this.testClassesDirectory); + return classesDirectories; + } + + @Override + protected boolean isUseTestClasspath() { + return true; + } + + @Override + protected RunArguments resolveJvmArguments() { + RunArguments jvmArguments = super.resolveJvmArguments(); + if (this.optimizedLaunch) { + jvmArguments.getArgs().addFirst("-XX:TieredStopAtLevel=1"); + } + return jvmArguments; + } + + @Override + protected void run(JavaProcessExecutor processExecutor, File workingDirectory, List args, + Map environmentVariables) throws MojoExecutionException, MojoFailureException { + processExecutor + .withRunProcessCustomizer( + (runProcess) -> Runtime.getRuntime().addShutdownHook(new Thread(new RunProcessKiller(runProcess)))) + .run(workingDirectory, args, environmentVariables); + } + + private static final class RunProcessKiller implements Runnable { + + private final RunProcess runProcess; + + private RunProcessKiller(RunProcess runProcess) { + this.runProcess = runProcess; + } + + @Override + public void run() { + this.runProcess.kill(); + } + + } + +}