Generate console output examples at build time
Closes gh-28208
This commit is contained in:
		
							parent
							
								
									7c0ccdf78c
								
							
						
					
					
						commit
						6efa77f3f5
					
				|  | @ -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<String> args = getProject().getObjects().listProperty(String.class); | ||||
| 
 | ||||
| 	private final Property<String> mainClass = getProject().getObjects().property(String.class); | ||||
| 
 | ||||
| 	private final Property<String> 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<String> getArgs() { | ||||
| 		return this.args; | ||||
| 	} | ||||
| 
 | ||||
| 	@Input | ||||
| 	public Property<String> getMainClass() { | ||||
| 		return this.mainClass; | ||||
| 	} | ||||
| 
 | ||||
| 	@Input | ||||
| 	public Property<String> getExpectedLogging() { | ||||
| 		return this.expectedLogging; | ||||
| 	} | ||||
| 
 | ||||
| 	@TaskAction | ||||
| 	void runApplication() throws IOException { | ||||
| 		List<String> 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<String> 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); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  | @ -14,6 +14,8 @@ configurations { | |||
| 	configurationProperties | ||||
| 	gradlePluginDocumentation | ||||
| 	mavenPluginDocumentation | ||||
| 	remoteSpringApplicationExample | ||||
| 	springApplicationExample | ||||
| 	testSlices | ||||
| } | ||||
| 
 | ||||
|  | @ -161,6 +163,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") | ||||
|  | @ -249,8 +259,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).)*$'] | ||||
| 	} | ||||
|  | @ -280,7 +317,10 @@ tasks.withType(org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask) { | |||
| 					"spring-kafka-version": versionConstraints["org.springframework.kafka:spring-kafka"], | ||||
| 					"spring-integration-version": versionConstraints["org.springframework.integration:spring-integration-core"], | ||||
| 					"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 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -12,24 +12,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 17321 --- [           main] o.s.b.d.s.s.SpringApplicationExample    : Starting SpringApplicationExample using Java 1.8.0_232 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 17321 --- [           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 (JVM running for 1.823) | ||||
| include::{spring-application-output}[] | ||||
| ---- | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -393,19 +393,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 (JVM 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. | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue