From f80490bafbf6f764b81908fba9a46ec39ab9772f Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Fri, 11 Feb 2022 14:44:28 -0600 Subject: [PATCH] Precompute Spring Boot version at build time Closes gh-29670 --- .../build/GeneratePropertiesResource.java | 81 +++++++++++++++++++ .../boot/build/JavaConventions.java | 49 ++++++----- .../boot/build/ConventionsPluginTests.java | 10 +++ .../boot/SpringBootVersion.java | 65 +++++---------- 4 files changed, 141 insertions(+), 64 deletions(-) create mode 100644 buildSrc/src/main/java/org/springframework/boot/build/GeneratePropertiesResource.java diff --git a/buildSrc/src/main/java/org/springframework/boot/build/GeneratePropertiesResource.java b/buildSrc/src/main/java/org/springframework/boot/build/GeneratePropertiesResource.java new file mode 100644 index 0000000000..7f766ac0a8 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/GeneratePropertiesResource.java @@ -0,0 +1,81 @@ +/* + * 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; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Map; + +import org.gradle.api.DefaultTask; +import org.gradle.api.Task; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; + +/** + * {@link Task} to generate properties and write them to disk as a properties file. + * + * @author Scott Frederick + */ +public class GeneratePropertiesResource extends DefaultTask { + + private final Property propertiesFileName; + + private final DirectoryProperty destinationDirectory; + + private final Map properties = new HashMap<>(); + + public GeneratePropertiesResource() { + ObjectFactory objects = getProject().getObjects(); + this.propertiesFileName = objects.property(String.class); + this.destinationDirectory = objects.directoryProperty(); + } + + @OutputDirectory + public DirectoryProperty getDestinationDirectory() { + return this.destinationDirectory; + } + + @Input + public Property getPropertiesFileName() { + return this.propertiesFileName; + } + + public void property(String name, String value) { + this.properties.put(name, value); + } + + @Input + public Map getProperties() { + return this.properties; + } + + @TaskAction + void generatePropertiesFile() throws IOException { + File outputFile = this.destinationDirectory.file(this.propertiesFileName).get().getAsFile(); + try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) { + this.properties.forEach((key, value) -> writer.printf("%s=%s\n", key, value)); + } + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java index d82ba98cec..8fd944299f 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * 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. @@ -104,6 +104,8 @@ class JavaConventions { private static final String SOURCE_AND_TARGET_COMPATIBILITY = "1.8"; + private static final String SPRING_BOOT_PROPERTIES_FILE = "spring-boot.properties"; + void apply(Project project) { project.getPlugins().withType(JavaBasePlugin.class, (java) -> { project.getPlugins().apply(TestFailuresPlugin.class); @@ -112,6 +114,7 @@ class JavaConventions { configureJavadocConventions(project); configureTestConventions(project); configureJarManifestConventions(project); + configureMetaInfResourcesConventions(project); configureDependencyManagement(project); configureToolchain(project); configureProhibitedDependencyChecks(project); @@ -119,29 +122,37 @@ class JavaConventions { } private void configureJarManifestConventions(Project project) { - ExtractResources extractLegalResources = project.getTasks().create("extractLegalResources", - ExtractResources.class); - extractLegalResources.getDestinationDirectory().set(project.getLayout().getBuildDirectory().dir("legal")); - extractLegalResources.setResourcesNames(Arrays.asList("LICENSE.txt", "NOTICE.txt")); - extractLegalResources.property("version", project.getVersion().toString()); SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); Set sourceJarTaskNames = sourceSets.stream().map(SourceSet::getSourcesJarTaskName) .collect(Collectors.toSet()); Set javadocJarTaskNames = sourceSets.stream().map(SourceSet::getJavadocJarTaskName) .collect(Collectors.toSet()); - project.getTasks().withType(Jar.class, (jar) -> project.afterEvaluate((evaluated) -> { - jar.metaInf((metaInf) -> metaInf.from(extractLegalResources)); - jar.manifest((manifest) -> { - Map attributes = new TreeMap<>(); - attributes.put("Automatic-Module-Name", project.getName().replace("-", ".")); - attributes.put("Build-Jdk-Spec", SOURCE_AND_TARGET_COMPATIBILITY); - attributes.put("Built-By", "Spring"); - attributes.put("Implementation-Title", - determineImplementationTitle(project, sourceJarTaskNames, javadocJarTaskNames, jar)); - attributes.put("Implementation-Version", project.getVersion()); - manifest.attributes(attributes); - }); - })); + project.getTasks().withType(Jar.class, + (jar) -> project.afterEvaluate((evaluated) -> jar.manifest((manifest) -> { + Map attributes = new TreeMap<>(); + attributes.put("Automatic-Module-Name", project.getName().replace("-", ".")); + attributes.put("Build-Jdk-Spec", SOURCE_AND_TARGET_COMPATIBILITY); + attributes.put("Built-By", "Spring"); + attributes.put("Implementation-Title", + determineImplementationTitle(project, sourceJarTaskNames, javadocJarTaskNames, jar)); + attributes.put("Implementation-Version", project.getVersion()); + manifest.attributes(attributes); + }))); + } + + private void configureMetaInfResourcesConventions(Project project) { + ExtractResources extractLegalResources = project.getTasks().create("extractLegalResources", + ExtractResources.class); + extractLegalResources.getDestinationDirectory().set(project.getLayout().getBuildDirectory().dir("legal")); + extractLegalResources.setResourcesNames(Arrays.asList("LICENSE.txt", "NOTICE.txt")); + extractLegalResources.property("version", project.getVersion().toString()); + GeneratePropertiesResource generateInfo = project.getTasks().create("generateSpringBootInfo", + GeneratePropertiesResource.class); + generateInfo.getDestinationDirectory().set(project.getLayout().getBuildDirectory().dir("info")); + generateInfo.getPropertiesFileName().set(SPRING_BOOT_PROPERTIES_FILE); + generateInfo.property("version", project.getVersion().toString()); + project.getTasks().withType(Jar.class, (jar) -> project.afterEvaluate( + (evaluated) -> jar.metaInf((metaInf) -> metaInf.from(extractLegalResources, generateInfo)))); } private String determineImplementationTitle(Project project, Set sourceJarTaskNames, diff --git a/buildSrc/src/test/java/org/springframework/boot/build/ConventionsPluginTests.java b/buildSrc/src/test/java/org/springframework/boot/build/ConventionsPluginTests.java index 8bfd16d586..9ac6a05dcd 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/ConventionsPluginTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/ConventionsPluginTests.java @@ -83,6 +83,7 @@ class ConventionsPluginTests { try (JarFile jar = new JarFile(file)) { assertThatLicenseIsPresent(jar); assertThatNoticeIsPresent(jar); + assertThatSpringBootPropertiesIsPresent(jar); Attributes mainAttributes = jar.getManifest().getMainAttributes(); assertThat(mainAttributes.getValue("Implementation-Title")) .isEqualTo("Test project for manifest customization"); @@ -112,6 +113,7 @@ class ConventionsPluginTests { try (JarFile jar = new JarFile(file)) { assertThatLicenseIsPresent(jar); assertThatNoticeIsPresent(jar); + assertThatSpringBootPropertiesIsPresent(jar); Attributes mainAttributes = jar.getManifest().getMainAttributes(); assertThat(mainAttributes.getValue("Implementation-Title")) .isEqualTo("Source for " + this.projectDir.getName()); @@ -141,6 +143,7 @@ class ConventionsPluginTests { try (JarFile jar = new JarFile(file)) { assertThatLicenseIsPresent(jar); assertThatNoticeIsPresent(jar); + assertThatSpringBootPropertiesIsPresent(jar); Attributes mainAttributes = jar.getManifest().getMainAttributes(); assertThat(mainAttributes.getValue("Implementation-Title")) .isEqualTo("Javadoc for " + this.projectDir.getName()); @@ -165,6 +168,13 @@ class ConventionsPluginTests { assertThat(noticeContent).doesNotContain("${"); } + private void assertThatSpringBootPropertiesIsPresent(JarFile jar) throws IOException { + JarEntry properties = jar.getJarEntry("META-INF/spring-boot.properties"); + assertThat(properties).isNotNull(); + String content = FileCopyUtils.copyToString(new InputStreamReader(jar.getInputStream(properties))); + assertThat(content).contains("version="); + } + @Test void testRetryIsConfiguredWithThreeRetriesOnCI() throws IOException { try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringBootVersion.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringBootVersion.java index dc5904d845..f507461800 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringBootVersion.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/SpringBootVersion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * 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. @@ -16,30 +16,22 @@ package org.springframework.boot; -import java.io.File; import java.io.IOException; -import java.net.JarURLConnection; -import java.net.URL; -import java.net.URLConnection; -import java.security.CodeSource; -import java.util.jar.Attributes; -import java.util.jar.Attributes.Name; -import java.util.jar.JarFile; +import java.io.InputStream; +import java.util.Properties; /** - * Class that exposes the Spring Boot version. Fetches the - * {@link Name#IMPLEMENTATION_VERSION Implementation-Version} manifest attribute from the - * jar file via {@link Package#getImplementationVersion()}, falling back to locating the - * jar file that contains this class and reading the {@code Implementation-Version} - * attribute from its manifest. - *

- * This class might not be able to determine the Spring Boot version in all environments. - * Consider using a reflection-based check instead: For example, checking for the presence - * of a specific Spring Boot method that you intend to call. + * Exposes the Spring Boot version. + * + * The version information is read from a file that is stored in the Spring Boot library. + * If the version information cannot be read from the file, consider using a + * reflection-based check instead (for example, checking for the presence of a specific + * Spring Boot method that you intend to call). * * @author Drummond Dawson * @author Hendrig Sellik * @author Andy Wilkinson + * @author Scott Frederick * @since 1.3.0 */ public final class SpringBootVersion { @@ -51,38 +43,21 @@ public final class SpringBootVersion { * Return the full version string of the present Spring Boot codebase, or {@code null} * if it cannot be determined. * @return the version of Spring Boot or {@code null} - * @see Package#getImplementationVersion() */ public static String getVersion() { - return determineSpringBootVersion(); - } - - private static String determineSpringBootVersion() { - String implementationVersion = SpringBootVersion.class.getPackage().getImplementationVersion(); - if (implementationVersion != null) { - return implementationVersion; - } - CodeSource codeSource = SpringBootVersion.class.getProtectionDomain().getCodeSource(); - if (codeSource == null) { - return null; - } - URL codeSourceLocation = codeSource.getLocation(); - try { - URLConnection connection = codeSourceLocation.openConnection(); - if (connection instanceof JarURLConnection) { - return getImplementationVersion(((JarURLConnection) connection).getJarFile()); + InputStream input = SpringBootVersion.class.getClassLoader() + .getResourceAsStream("META-INF/spring-boot.properties"); + if (input != null) { + try { + Properties properties = new Properties(); + properties.load(input); + return properties.getProperty("version"); } - try (JarFile jarFile = new JarFile(new File(codeSourceLocation.toURI()))) { - return getImplementationVersion(jarFile); + catch (IOException ex) { + // fall through } } - catch (Exception ex) { - return null; - } - } - - private static String getImplementationVersion(JarFile jarFile) throws IOException { - return jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); + return null; } }