diff --git a/buildSrc/src/main/java/org/springframework/boot/build/docs/ApplicationRunner.java b/buildSrc/src/main/java/org/springframework/boot/build/docs/ApplicationRunner.java new file mode 100644 index 0000000000..960fed7744 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/docs/ApplicationRunner.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2022 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.build.docs; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Task; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Classpath; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.gradle.internal.jvm.Jvm; + +/** + * {@link Task} to run an application for the purpose of capturing its output for + * inclusion in the reference documentation. + * + * @author Andy Wilkinson + */ +public class ApplicationRunner extends DefaultTask { + + private final RegularFileProperty output = getProject().getObjects().fileProperty(); + + private final ListProperty args = getProject().getObjects().listProperty(String.class); + + private final Property mainClass = getProject().getObjects().property(String.class); + + private final Property expectedLogging = getProject().getObjects().property(String.class); + + private FileCollection classpath; + + @OutputFile + public RegularFileProperty getOutput() { + return this.output; + } + + @Classpath + public FileCollection getClasspath() { + return this.classpath; + } + + public void setClasspath(FileCollection classpath) { + this.classpath = classpath; + } + + @Input + public ListProperty getArgs() { + return this.args; + } + + @Input + public Property getMainClass() { + return this.mainClass; + } + + @Input + public Property getExpectedLogging() { + return this.expectedLogging; + } + + @TaskAction + void runApplication() throws IOException { + List command = new ArrayList<>(); + File executable = Jvm.current().getExecutable("java"); + command.add(executable.getAbsolutePath()); + command.add("-cp"); + command.add(this.classpath.getFiles().stream().map(File::getAbsolutePath) + .collect(Collectors.joining(File.pathSeparator))); + command.add(this.mainClass.get()); + command.addAll(this.args.get()); + File outputFile = this.output.getAsFile().get(); + Process process = new ProcessBuilder().redirectOutput(outputFile).redirectError(outputFile).command(command) + .start(); + awaitLogging(process); + process.destroy(); + } + + private void awaitLogging(Process process) { + long end = System.currentTimeMillis() + 30000; + String expectedLogging = this.expectedLogging.get(); + while (System.currentTimeMillis() < end) { + for (String line : outputLines()) { + if (line.contains(expectedLogging)) { + return; + } + } + if (!process.isAlive()) { + throw new IllegalStateException("Process exited before '" + expectedLogging + "' was logged"); + } + } + throw new IllegalStateException("'" + expectedLogging + "' was not logged within 30 seconds"); + } + + private List outputLines() { + Path outputPath = this.output.get().getAsFile().toPath(); + try { + return Files.readAllLines(outputPath); + } + catch (IOException ex) { + throw new RuntimeException("Failed to read lines of output from '" + outputPath + "'", ex); + } + } + +} diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 80bbcc7044..416df8d7d0 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -15,6 +15,8 @@ configurations { configurationProperties gradlePluginDocumentation mavenPluginDocumentation + remoteSpringApplicationExample + springApplicationExample testSlices } @@ -159,6 +161,14 @@ dependencies { mavenPluginDocumentation(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-maven-plugin", configuration: "documentation")) + remoteSpringApplicationExample(platform(project(":spring-boot-project:spring-boot-dependencies"))) + remoteSpringApplicationExample(project(":spring-boot-project:spring-boot-devtools")) + remoteSpringApplicationExample(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-logging")) + remoteSpringApplicationExample("org.springframework:spring-web") + + springApplicationExample(platform(project(":spring-boot-project:spring-boot-dependencies"))) + springApplicationExample(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + testImplementation(project(":spring-boot-project:spring-boot-actuator-autoconfigure")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("org.assertj:assertj-core") @@ -245,8 +255,35 @@ task documentConfigurationProperties(type: org.springframework.boot.build.contex task documentDevtoolsPropertyDefaults(type: org.springframework.boot.build.devtools.DocumentDevtoolsPropertyDefaults) {} +task runRemoteSpringApplicationExample(type: org.springframework.boot.build.docs.ApplicationRunner) { + classpath = configurations.remoteSpringApplicationExample + mainClass = "org.springframework.boot.devtools.RemoteSpringApplication" + args = ["https://myapp.example.com", "--spring.devtools.remote.secret=secret", "--spring.devtools.livereload.port=0"] + output = file("$buildDir/example-output/remote-spring-application.txt") + expectedLogging = "Started RemoteSpringApplication in " +} + +task runSpringApplicationExample(type: org.springframework.boot.build.docs.ApplicationRunner) { + classpath = configurations.springApplicationExample + sourceSets.main.output + mainClass = "org.springframework.boot.docs.features.springapplication.MyApplication" + args = ["--server.port=0"] + output = file("$buildDir/example-output/spring-application.txt") + expectedLogging = "Started MyApplication in " +} + +task runLoggingFormatExample(type: org.springframework.boot.build.docs.ApplicationRunner) { + classpath = configurations.springApplicationExample + sourceSets.main.output + mainClass = "org.springframework.boot.docs.features.springapplication.MyApplication" + args = ["--spring.main.banner-mode=off", "--server.port=0"] + output = file("$buildDir/example-output/logging-format.txt") + expectedLogging = "Started MyApplication in " +} + tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { dependsOn dependencyVersions + inputs.files(runRemoteSpringApplicationExample).withPathSensitivity(PathSensitivity.RELATIVE) + inputs.files(runSpringApplicationExample).withPathSensitivity(PathSensitivity.RELATIVE) + inputs.files(runLoggingFormatExample).withPathSensitivity(PathSensitivity.RELATIVE) asciidoctorj { fatalWarnings = ['^((?!successfully validated).)*$'] } @@ -276,7 +313,10 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { "spring-integration-version": versionConstraints["org.springframework.integration:spring-integration-core"], "spring-kafka-version": versionConstraints["org.springframework.kafka:spring-kafka"], "spring-security-version": securityVersion, - "spring-webservices-version": versionConstraints["org.springframework.ws:spring-ws-core"] + "spring-webservices-version": versionConstraints["org.springframework.ws:spring-ws-core"], + "remote-spring-application-output": runRemoteSpringApplicationExample.outputs.files.singleFile, + "spring-application-output": runSpringApplicationExample.outputs.files.singleFile, + "logging-format-output": runLoggingFormatExample.outputs.files.singleFile } } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc index b83de101f2..6876bc3216 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc @@ -22,11 +22,7 @@ The default log output from Spring Boot resembles the following example: [indent=0] ---- -2019-03-05 10:57:51.112 INFO 45469 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/7.0.52 -2019-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext -2019-03-05 10:57:51.253 INFO 45469 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1358 ms -2019-03-05 10:57:51.698 INFO 45469 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/] -2019-03-05 10:57:51.702 INFO 45469 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*] +include::{logging-format-output}[] ---- The following items are output: diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc index 8224b38ac1..f8c51f3344 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/spring-application.adoc @@ -9,24 +9,7 @@ When your application starts, you should see something similar to the following [indent=0,subs="verbatim,attributes"] ---- - . ____ _ __ _ _ - /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ -( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ - \\/ ___)| |_)| | | | | || (_| | ) ) ) ) - ' |____| .__|_| |_|_| |_\__, | / / / / - =========|_|==============|___/=/_/_/_/ - :: Spring Boot :: v{spring-boot-version} - -2021-02-03 10:33:25.224 INFO 17900 --- [ main] o.s.b.d.s.s.SpringApplicationExample : Starting SpringAppplicationExample using Java 17 on mycomputer with PID 17321 (/apps/myjar.jar started by pwebb) -2021-02-03 10:33:25.226 INFO 17900 --- [ main] o.s.b.d.s.s.SpringApplicationExample : No active profile set, falling back to default profiles: default -2021-02-03 10:33:26.046 INFO 17900 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) -2021-02-03 10:33:26.054 INFO 17900 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] -2021-02-03 10:33:26.055 INFO 17900 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.41] -2021-02-03 10:33:26.097 INFO 17900 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext -2021-02-03 10:33:26.097 INFO 17900 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 821 ms -2021-02-03 10:33:26.144 INFO 17900 --- [ main] s.tomcat.SampleTomcatApplication : ServletContext initialized -2021-02-03 10:33:26.376 INFO 17900 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' -2021-02-03 10:33:26.384 INFO 17900 --- [ main] o.s.b.d.s.s.SpringApplicationExample : Started SampleTomcatApplication in 1.514 seconds (process running for 1.823) +include::{spring-application-output}[] ---- diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/devtools.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/devtools.adoc index 3611581545..017814e81d 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/devtools.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/using/devtools.adoc @@ -390,19 +390,7 @@ A running remote client might resemble the following listing: [indent=0,subs="verbatim,attributes"] ---- - . ____ _ __ _ _ - /\\ / ___'_ __ _ _(_)_ __ __ _ ___ _ \ \ \ \ - ( ( )\___ | '_ | '_| | '_ \/ _` | | _ \___ _ __ ___| |_ ___ \ \ \ \ - \\/ ___)| |_)| | | | | || (_| []::::::[] / -_) ' \/ _ \ _/ -_) ) ) ) ) - ' |____| .__|_| |_|_| |_\__, | |_|_\___|_|_|_\___/\__\___|/ / / / - =========|_|==============|___/===================================/_/_/_/ - :: Spring Boot Remote :: {spring-boot-version} - - 2015-06-10 18:25:06.632 INFO 14938 --- [ main] o.s.b.devtools.RemoteSpringApplication : Starting RemoteSpringApplication on pwmbp with PID 14938 (/Users/pwebb/projects/spring-boot/code/spring-boot-project/spring-boot-devtools/target/classes started by pwebb in /Users/pwebb/projects/spring-boot/code) - 2015-06-10 18:25:06.671 INFO 14938 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@2a17b7b6: startup date [Wed Jun 10 18:25:06 PDT 2015]; root of context hierarchy - 2015-06-10 18:25:07.043 WARN 14938 --- [ main] o.s.b.d.r.c.RemoteClientConfiguration : The connection to http://localhost:8080 is insecure. You should use a URL starting with 'https://'. - 2015-06-10 18:25:07.074 INFO 14938 --- [ main] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729 - 2015-06-10 18:25:07.130 INFO 14938 --- [ main] o.s.b.devtools.RemoteSpringApplication : Started RemoteSpringApplication in 0.74 seconds (process running for 1.105) +include::{remote-spring-application-output}[] ---- NOTE: Because the remote client is using the same classpath as the real application it can directly read application properties.