mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-10-08 10:19:13 +00:00
Preserve property descriptions during Gradle incremental compilation
Cache descriptions in a location outside CLASS_OUTPUT so that Gradle's incremental compilation does not lose javadoc-based descriptions for types passed as .class files. See gh-50489 Signed-off-by: Agustin Bereciartua <bereciartua.agustin@gmail.com>
This commit is contained in:
committed by
Andy Wilkinson
parent
01afdc8dc7
commit
f135b3fb88
+1
@@ -24,6 +24,7 @@ When Gradle's {url-gradle-docs-java-plugin}[`java` plugin] is applied to a proje
|
||||
10. Creates a configuration named `productionRuntimeClasspath`. It is equivalent to `runtimeClasspath` minus any dependencies that only appear in the `developmentOnly` or `testDevelopmentOnly` configurations.
|
||||
11. Configures any `JavaCompile` tasks with no configured encoding to use `UTF-8`.
|
||||
12. Configures any `JavaCompile` tasks to use the `-parameters` compiler argument.
|
||||
13. Configures any `JavaCompile` tasks that use `spring-boot-configuration-processor` to preserve property descriptions across incremental compilations by caching the previously generated metadata in the task's temporary directory.
|
||||
|
||||
|
||||
|
||||
|
||||
+18
-2
@@ -278,7 +278,12 @@ final class JavaPluginAction implements PluginApplicationAction {
|
||||
.filter((candidate) -> candidate.getCompileJavaTaskName().equals(compile.getName()))
|
||||
.map((match) -> match.getResources().getSrcDirs())
|
||||
.findFirst()
|
||||
.ifPresent((locations) -> compile.doFirst(new AdditionalMetadataLocationsConfigurer(locations)));
|
||||
.ifPresent((locations) -> {
|
||||
File descriptionCache = new File(compile.getTemporaryDir(),
|
||||
"previous-spring-configuration-metadata.json");
|
||||
compile.getOutputs().file(descriptionCache);
|
||||
compile.doFirst(new AdditionalMetadataLocationsConfigurer(locations, descriptionCache));
|
||||
});
|
||||
}
|
||||
|
||||
private void configureProductionRuntimeClasspathConfiguration(Project project) {
|
||||
@@ -350,8 +355,11 @@ final class JavaPluginAction implements PluginApplicationAction {
|
||||
|
||||
private final Set<File> locations;
|
||||
|
||||
private AdditionalMetadataLocationsConfigurer(Set<File> locations) {
|
||||
private final File descriptionCache;
|
||||
|
||||
private AdditionalMetadataLocationsConfigurer(Set<File> locations, File descriptionCache) {
|
||||
this.locations = locations;
|
||||
this.descriptionCache = descriptionCache;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -361,6 +369,7 @@ final class JavaPluginAction implements PluginApplicationAction {
|
||||
}
|
||||
if (hasConfigurationProcessorOnClasspath(compile)) {
|
||||
configureAdditionalMetadataLocations(compile);
|
||||
configureDescriptionCacheLocation(compile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,6 +388,13 @@ final class JavaPluginAction implements PluginApplicationAction {
|
||||
+ StringUtils.collectionToCommaDelimitedString(this.locations));
|
||||
}
|
||||
|
||||
private void configureDescriptionCacheLocation(JavaCompile compile) {
|
||||
compile.getOptions()
|
||||
.getCompilerArgs()
|
||||
.add("-Aorg.springframework.boot.configurationprocessor.descriptionCacheLocation="
|
||||
+ this.descriptionCache.getAbsolutePath());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-3
@@ -139,9 +139,28 @@ class JavaPluginActionIntegrationTests {
|
||||
BuildTask task = result.task(":compileJava");
|
||||
assertThat(task).isNotNull();
|
||||
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("compileJava compiler args: [-parameters, -Aorg.springframework.boot."
|
||||
+ "configurationprocessor.additionalMetadataLocations="
|
||||
+ new File(this.gradleBuild.getProjectDir(), "src/main/resources").getCanonicalPath());
|
||||
assertThat(result.getOutput())
|
||||
.contains("compileJava compiler args: [-parameters, -Aorg.springframework.boot."
|
||||
+ "configurationprocessor.additionalMetadataLocations="
|
||||
+ new File(this.gradleBuild.getProjectDir(), "src/main/resources").getCanonicalPath()
|
||||
+ ", -Aorg.springframework.boot.configurationprocessor.descriptionCacheLocation="
|
||||
+ new File(this.gradleBuild.getProjectDir(),
|
||||
"build/tmp/compileJava/previous-spring-configuration-metadata.json")
|
||||
.getCanonicalPath()
|
||||
+ "]");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void descriptionCacheLocationIsAnOutputOfCompileJava() throws IOException {
|
||||
createMinimalMainSource();
|
||||
BuildResult result = this.gradleBuild.build("compileJava");
|
||||
BuildTask task = result.task(":compileJava");
|
||||
assertThat(task).isNotNull();
|
||||
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("compileJava outputs: ")
|
||||
.contains(new File(this.gradleBuild.getProjectDir(),
|
||||
"build/tmp/compileJava/previous-spring-configuration-metadata.json")
|
||||
.getCanonicalPath());
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
doLast {
|
||||
println "${name} outputs: ${outputs.files.files}"
|
||||
}
|
||||
}
|
||||
+40
-1
@@ -21,6 +21,7 @@ import java.io.StringWriter;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -77,6 +78,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
|
||||
static final String ADDITIONAL_METADATA_LOCATIONS_OPTION = "org.springframework.boot.configurationprocessor.additionalMetadataLocations";
|
||||
|
||||
static final String DESCRIPTION_CACHE_LOCATION_OPTION = "org.springframework.boot.configurationprocessor.descriptionCacheLocation";
|
||||
|
||||
static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationProperties";
|
||||
|
||||
static final String CONFIGURATION_PROPERTIES_SOURCE_ANNOTATION = "org.springframework.boot.context.properties.ConfigurationPropertiesSource";
|
||||
@@ -113,7 +116,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
|
||||
static final String ENDPOINT_ACCESS_ENUM = "org.springframework.boot.actuate.endpoint.Access";
|
||||
|
||||
private static final Set<String> SUPPORTED_OPTIONS = Set.of(ADDITIONAL_METADATA_LOCATIONS_OPTION);
|
||||
private static final Set<String> SUPPORTED_OPTIONS = Set.of(ADDITIONAL_METADATA_LOCATIONS_OPTION,
|
||||
DESCRIPTION_CACHE_LOCATION_OPTION);
|
||||
|
||||
private MetadataStore metadataStore;
|
||||
|
||||
@@ -123,6 +127,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
|
||||
private MetadataGenerationEnvironment metadataEnv;
|
||||
|
||||
private DescriptionCache descriptionCache;
|
||||
|
||||
private final Set<String> classFileBackedTypes = new HashSet<>();
|
||||
|
||||
protected String configurationPropertiesAnnotation() {
|
||||
return CONFIGURATION_PROPERTIES_ANNOTATION;
|
||||
}
|
||||
@@ -189,6 +197,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
configurationPropertiesSourceAnnotation(), nestedConfigurationPropertyAnnotation(),
|
||||
deprecatedConfigurationPropertyAnnotation(), constructorBindingAnnotation(), autowiredAnnotation(),
|
||||
defaultValueAnnotation(), endpointAnnotations(), readOperationAnnotation(), nameAnnotation());
|
||||
String cacheLocation = env.getOptions().get(DESCRIPTION_CACHE_LOCATION_OPTION);
|
||||
if (cacheLocation != null) {
|
||||
this.descriptionCache = new DescriptionCache(cacheLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -289,6 +301,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
Deque<TypeElement> seen) {
|
||||
if (!seen.contains(element)) {
|
||||
seen.push(element);
|
||||
if (this.descriptionCache != null && !this.metadataEnv.hasSourceTree(element)) {
|
||||
this.classFileBackedTypes.add(this.metadataEnv.getTypeUtils().getQualifiedName(element));
|
||||
}
|
||||
new PropertyDescriptorResolver(this.metadataEnv).resolve(element, source).forEach((descriptor) -> {
|
||||
this.metadataCollector.add(descriptor.resolveItemMetadata(prefix, this.metadataEnv));
|
||||
ItemHint itemHint = descriptor.resolveItemHint(prefix, this.metadataEnv);
|
||||
@@ -405,14 +420,38 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
|
||||
protected ConfigurationMetadata writeMetadata() throws Exception {
|
||||
ConfigurationMetadata metadata = this.metadataCollector.getMetadata();
|
||||
metadata = mergeAdditionalMetadata(metadata, () -> this.metadataStore.readAdditionalMetadata());
|
||||
fillCachedDescriptions(metadata);
|
||||
removeIgnored(metadata);
|
||||
if (!metadata.getItems().isEmpty()) {
|
||||
this.metadataStore.writeMetadata(metadata);
|
||||
updateDescriptionCache(metadata);
|
||||
return metadata;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void fillCachedDescriptions(ConfigurationMetadata metadata) {
|
||||
if (this.descriptionCache == null) {
|
||||
return;
|
||||
}
|
||||
for (ItemMetadata item : metadata.getItems()) {
|
||||
if (item.isOfItemType(ItemMetadata.ItemType.PROPERTY) && item.getDescription() == null
|
||||
&& item.getSourceType() != null && this.classFileBackedTypes.contains(item.getSourceType())) {
|
||||
String cached = this.descriptionCache.getDescription(item.getName());
|
||||
if (cached != null) {
|
||||
item.setDescription(cached);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDescriptionCache(ConfigurationMetadata metadata) {
|
||||
if (this.descriptionCache == null) {
|
||||
return;
|
||||
}
|
||||
this.descriptionCache.update(metadata);
|
||||
}
|
||||
|
||||
private void removeIgnored(ConfigurationMetadata metadata) {
|
||||
for (ItemIgnore itemIgnore : metadata.getIgnored()) {
|
||||
metadata.removeMetadata(itemIgnore.getType(), itemIgnore.getName());
|
||||
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.configurationprocessor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
|
||||
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
|
||||
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
|
||||
|
||||
/**
|
||||
* Cache for property descriptions that persists across incremental builds. Stores
|
||||
* metadata in a location outside of {@code CLASS_OUTPUT} so that Gradle's incremental
|
||||
* compilation does not delete it.
|
||||
*
|
||||
* @author Agustin Palazzo
|
||||
* @see ConfigurationMetadataAnnotationProcessor
|
||||
*/
|
||||
class DescriptionCache {
|
||||
|
||||
private final File cacheFile;
|
||||
|
||||
private ConfigurationMetadata cachedMetadata;
|
||||
|
||||
DescriptionCache(String cacheFilePath) {
|
||||
this.cacheFile = new File(cacheFilePath);
|
||||
this.cachedMetadata = read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a cached description for the given property name.
|
||||
* @param propertyName the fully qualified property name
|
||||
* @return the cached description or {@code null}
|
||||
*/
|
||||
String getDescription(String propertyName) {
|
||||
if (this.cachedMetadata == null) {
|
||||
return null;
|
||||
}
|
||||
for (ItemMetadata item : this.cachedMetadata.getItems()) {
|
||||
if (item.isOfItemType(ItemMetadata.ItemType.PROPERTY) && propertyName.equals(item.getName())) {
|
||||
return item.getDescription();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the cache with the given metadata. After
|
||||
* {@link ConfigurationMetadataAnnotationProcessor#fillCachedDescriptions} has
|
||||
* restored descriptions from the cache, the metadata is the complete picture of the
|
||||
* current build. Replacing (instead of merging) ensures that entries for deleted or
|
||||
* de-annotated types are automatically pruned.
|
||||
* @param metadata the current build's metadata with descriptions already filled
|
||||
*/
|
||||
void update(ConfigurationMetadata metadata) {
|
||||
write(metadata);
|
||||
this.cachedMetadata = new ConfigurationMetadata(metadata);
|
||||
}
|
||||
|
||||
private ConfigurationMetadata read() {
|
||||
if (!this.cacheFile.exists()) {
|
||||
return null;
|
||||
}
|
||||
try (FileInputStream in = new FileInputStream(this.cacheFile)) {
|
||||
return new JsonMarshaller().read(in);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void write(ConfigurationMetadata metadata) {
|
||||
this.cacheFile.getParentFile().mkdirs();
|
||||
try (FileOutputStream out = new FileOutputStream(this.cacheFile)) {
|
||||
new JsonMarshaller().write(metadata, out);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// Cache write failure is non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+4
@@ -137,6 +137,10 @@ class MetadataGenerationEnvironment {
|
||||
return this.typeUtils;
|
||||
}
|
||||
|
||||
boolean hasSourceTree(TypeElement element) {
|
||||
return this.fieldValuesParser.hasSourceTree(element);
|
||||
}
|
||||
|
||||
Messager getMessager() {
|
||||
return this.messager;
|
||||
}
|
||||
|
||||
+20
-2
@@ -30,13 +30,19 @@ import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCom
|
||||
* @since 1.1.2
|
||||
* @see JavaCompilerFieldValuesParser
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FieldValuesParser {
|
||||
|
||||
/**
|
||||
* Implementation of {@link FieldValuesParser} that always returns an empty result.
|
||||
*/
|
||||
FieldValuesParser NONE = (element) -> Collections.emptyMap();
|
||||
FieldValuesParser NONE = new FieldValuesParser() {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getFieldValues(TypeElement element) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the field values for the given element.
|
||||
@@ -46,4 +52,16 @@ public interface FieldValuesParser {
|
||||
*/
|
||||
Map<String, Object> getFieldValues(TypeElement element) throws Exception;
|
||||
|
||||
/**
|
||||
* Return whether the given element has a source tree available. Elements loaded from
|
||||
* {@code .class} files during incremental compilation will not have a source tree,
|
||||
* meaning javadoc comments are unavailable.
|
||||
* @param element the element to check
|
||||
* @return {@code true} if the element has source, {@code false} if backed by
|
||||
* {@code .class}
|
||||
*/
|
||||
default boolean hasSourceTree(TypeElement element) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
@@ -57,6 +57,16 @@ public class JavaCompilerFieldValuesParser implements FieldValuesParser {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSourceTree(TypeElement element) {
|
||||
try {
|
||||
return this.trees.getTree(element) != null;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link TreeVisitor} to collect fields.
|
||||
*/
|
||||
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.configurationprocessor;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.tools.JavaCompiler;
|
||||
import javax.tools.JavaFileObject;
|
||||
import javax.tools.SimpleJavaFileObject;
|
||||
import javax.tools.StandardJavaFileManager;
|
||||
import javax.tools.StandardLocation;
|
||||
import javax.tools.ToolProvider;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
|
||||
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
|
||||
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
|
||||
import org.springframework.boot.configurationprocessor.test.TestConfigurationMetadataAnnotationProcessor;
|
||||
import org.springframework.boot.configurationsample.incremental.BarProperties;
|
||||
import org.springframework.boot.configurationsample.incremental.FooProperties;
|
||||
import org.springframework.boot.configurationsample.record.ExampleRecord;
|
||||
import org.springframework.core.test.tools.SourceFile;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DescriptionCache} verifying that property descriptions survive
|
||||
* Gradle-style incremental compilation where unchanged types arrive as {@code .class}
|
||||
* files (without javadoc).
|
||||
*
|
||||
* @author Agustin Palazzo
|
||||
*/
|
||||
class DescriptionCacheTests {
|
||||
|
||||
private static final String METADATA_PATH = "META-INF/spring-configuration-metadata.json";
|
||||
|
||||
@Test
|
||||
void incrementalBuildWithoutCacheLosesDescription(@TempDir Path tempDir) throws Exception {
|
||||
Path classOutput = tempDir.resolve("classes");
|
||||
Path sourceOutput = tempDir.resolve("sources");
|
||||
Files.createDirectories(classOutput);
|
||||
Files.createDirectories(sourceOutput);
|
||||
|
||||
String classpath = System.getProperty("java.class.path");
|
||||
String fooSource = SourceFile.forTestClass(FooProperties.class).getContent();
|
||||
String barSource = SourceFile.forTestClass(BarProperties.class).getContent();
|
||||
|
||||
List<JavaFileObject> allSources = List.of(
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.FooProperties", fooSource),
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.BarProperties", barSource));
|
||||
|
||||
boolean ok = compile(classOutput, sourceOutput, classpath, null, allSources, null);
|
||||
assertThat(ok).as("Full compilation").isTrue();
|
||||
assertThat(descriptionOf(readMetadata(classOutput), "foo.counter")).isEqualTo("A nice counter description.");
|
||||
assertThat(descriptionOf(readMetadata(classOutput), "bar.counter")).isEqualTo("A nice counter description.");
|
||||
|
||||
Files.deleteIfExists(classOutput.resolve(METADATA_PATH));
|
||||
|
||||
String incrementalCp = classpath + File.pathSeparator + classOutput;
|
||||
List<JavaFileObject> barOnly = List.of(
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.BarProperties", barSource));
|
||||
List<String> fooAsClass = List
|
||||
.of("org.springframework.boot.configurationsample.incremental.FooProperties");
|
||||
|
||||
ok = compile(classOutput, sourceOutput, incrementalCp, null, barOnly, fooAsClass);
|
||||
assertThat(ok).as("Incremental compilation").isTrue();
|
||||
|
||||
ConfigurationMetadata incremental = readMetadata(classOutput);
|
||||
assertThat(descriptionOf(incremental, "bar.counter")).isEqualTo("A nice counter description.");
|
||||
assertThat(descriptionOf(incremental, "foo.counter")).as("BUG: description lost without cache").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void incrementalBuildWithCachePreservesDescription(@TempDir Path tempDir) throws Exception {
|
||||
Path classOutput = tempDir.resolve("classes");
|
||||
Path sourceOutput = tempDir.resolve("sources");
|
||||
Path cacheFile = tempDir.resolve("description-cache.json");
|
||||
Files.createDirectories(classOutput);
|
||||
Files.createDirectories(sourceOutput);
|
||||
|
||||
String classpath = System.getProperty("java.class.path");
|
||||
String fooSource = SourceFile.forTestClass(FooProperties.class).getContent();
|
||||
String barSource = SourceFile.forTestClass(BarProperties.class).getContent();
|
||||
|
||||
List<JavaFileObject> allSources = List.of(
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.FooProperties", fooSource),
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.BarProperties", barSource));
|
||||
|
||||
boolean ok = compile(classOutput, sourceOutput, classpath, cacheFile, allSources, null);
|
||||
assertThat(ok).as("Full compilation").isTrue();
|
||||
assertThat(descriptionOf(readMetadata(classOutput), "foo.counter")).isEqualTo("A nice counter description.");
|
||||
assertThat(descriptionOf(readMetadata(classOutput), "bar.counter")).isEqualTo("A nice counter description.");
|
||||
assertThat(Files.exists(cacheFile)).as("Cache file created").isTrue();
|
||||
|
||||
Files.deleteIfExists(classOutput.resolve(METADATA_PATH));
|
||||
|
||||
String incrementalCp = classpath + File.pathSeparator + classOutput;
|
||||
List<JavaFileObject> barOnly = List.of(
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.BarProperties", barSource));
|
||||
List<String> fooAsClass = List
|
||||
.of("org.springframework.boot.configurationsample.incremental.FooProperties");
|
||||
|
||||
ok = compile(classOutput, sourceOutput, incrementalCp, cacheFile, barOnly, fooAsClass);
|
||||
assertThat(ok).as("Incremental compilation").isTrue();
|
||||
|
||||
ConfigurationMetadata incremental = readMetadata(classOutput);
|
||||
assertThat(descriptionOf(incremental, "bar.counter")).isEqualTo("A nice counter description.");
|
||||
assertThat(descriptionOf(incremental, "foo.counter")).as("Description preserved from cache")
|
||||
.isEqualTo("A nice counter description.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cacheIsPrunedWhenTypeIsRemoved(@TempDir Path tempDir) throws Exception {
|
||||
Path classOutput = tempDir.resolve("classes");
|
||||
Path sourceOutput = tempDir.resolve("sources");
|
||||
Path cacheFile = tempDir.resolve("description-cache.json");
|
||||
Files.createDirectories(classOutput);
|
||||
Files.createDirectories(sourceOutput);
|
||||
|
||||
String classpath = System.getProperty("java.class.path");
|
||||
String fooSource = SourceFile.forTestClass(FooProperties.class).getContent();
|
||||
String barSource = SourceFile.forTestClass(BarProperties.class).getContent();
|
||||
|
||||
List<JavaFileObject> allSources = List.of(
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.FooProperties", fooSource),
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.BarProperties", barSource));
|
||||
|
||||
boolean ok = compile(classOutput, sourceOutput, classpath, cacheFile, allSources, null);
|
||||
assertThat(ok).as("Full compilation").isTrue();
|
||||
|
||||
ConfigurationMetadata cache1 = readJson(cacheFile);
|
||||
assertThat(descriptionOf(cache1, "foo.counter")).isEqualTo("A nice counter description.");
|
||||
assertThat(descriptionOf(cache1, "bar.counter")).isEqualTo("A nice counter description.");
|
||||
|
||||
Files.deleteIfExists(classOutput.resolve(METADATA_PATH));
|
||||
|
||||
List<JavaFileObject> barOnly = List.of(
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.BarProperties", barSource));
|
||||
ok = compile(classOutput, sourceOutput, classpath, cacheFile, barOnly, null);
|
||||
assertThat(ok).as("Compile without FooProperties").isTrue();
|
||||
|
||||
ConfigurationMetadata cache2 = readJson(cacheFile);
|
||||
assertThat(descriptionOf(cache2, "bar.counter")).isEqualTo("A nice counter description.");
|
||||
assertThat(descriptionOf(cache2, "foo.counter")).as("Pruned from cache").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void incrementalBuildWithCachePreservesRecordDescription(@TempDir Path tempDir) throws Exception {
|
||||
Path classOutput = tempDir.resolve("classes");
|
||||
Path sourceOutput = tempDir.resolve("sources");
|
||||
Path cacheFile = tempDir.resolve("description-cache.json");
|
||||
Files.createDirectories(classOutput);
|
||||
Files.createDirectories(sourceOutput);
|
||||
|
||||
String classpath = System.getProperty("java.class.path");
|
||||
String recordSource = SourceFile.forTestClass(ExampleRecord.class).getContent();
|
||||
String barSource = SourceFile.forTestClass(BarProperties.class).getContent();
|
||||
|
||||
List<JavaFileObject> allSources = List.of(
|
||||
inMemorySource("org.springframework.boot.configurationsample.record.ExampleRecord", recordSource),
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.BarProperties", barSource));
|
||||
|
||||
boolean ok = compile(classOutput, sourceOutput, classpath, cacheFile, allSources, null);
|
||||
assertThat(ok).as("Full compilation").isTrue();
|
||||
assertThat(descriptionOf(readMetadata(classOutput), "record.descriptions.some-string"))
|
||||
.isEqualTo("very long description that doesn't fit single line and is indented");
|
||||
|
||||
Files.deleteIfExists(classOutput.resolve(METADATA_PATH));
|
||||
|
||||
String incrementalCp = classpath + File.pathSeparator + classOutput;
|
||||
List<JavaFileObject> barOnly = List.of(
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.BarProperties", barSource));
|
||||
List<String> recordAsClass = List
|
||||
.of("org.springframework.boot.configurationsample.record.ExampleRecord");
|
||||
|
||||
ok = compile(classOutput, sourceOutput, incrementalCp, cacheFile, barOnly, recordAsClass);
|
||||
assertThat(ok).as("Incremental compilation").isTrue();
|
||||
|
||||
ConfigurationMetadata incremental = readMetadata(classOutput);
|
||||
assertThat(descriptionOf(incremental, "bar.counter")).isEqualTo("A nice counter description.");
|
||||
assertThat(descriptionOf(incremental, "record.descriptions.some-string"))
|
||||
.as("Record description preserved from cache")
|
||||
.isEqualTo("very long description that doesn't fit single line and is indented");
|
||||
}
|
||||
|
||||
@Test
|
||||
void corruptCacheFileIsIgnored(@TempDir Path tempDir) throws Exception {
|
||||
Path classOutput = tempDir.resolve("classes");
|
||||
Path sourceOutput = tempDir.resolve("sources");
|
||||
Path cacheFile = tempDir.resolve("description-cache.json");
|
||||
Files.createDirectories(classOutput);
|
||||
Files.createDirectories(sourceOutput);
|
||||
Files.writeString(cacheFile, "{ this is not valid json !!!");
|
||||
|
||||
String classpath = System.getProperty("java.class.path");
|
||||
String fooSource = SourceFile.forTestClass(FooProperties.class).getContent();
|
||||
|
||||
List<JavaFileObject> sources = List.of(
|
||||
inMemorySource("org.springframework.boot.configurationsample.incremental.FooProperties", fooSource));
|
||||
|
||||
boolean ok = compile(classOutput, sourceOutput, classpath, cacheFile, sources, null);
|
||||
assertThat(ok).as("Compilation with corrupt cache").isTrue();
|
||||
|
||||
ConfigurationMetadata metadata = readMetadata(classOutput);
|
||||
assertThat(metadata).isNotNull();
|
||||
assertThat(descriptionOf(metadata, "foo.counter")).isEqualTo("A nice counter description.");
|
||||
}
|
||||
|
||||
private boolean compile(Path classOutput, Path sourceOutput, String classpath, Path cacheFile,
|
||||
List<JavaFileObject> sources, List<String> classes) throws IOException {
|
||||
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
|
||||
try (StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null)) {
|
||||
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(classOutput.toFile()));
|
||||
fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, List.of(sourceOutput.toFile()));
|
||||
fileManager.setLocation(StandardLocation.CLASS_PATH, classpathFiles(classpath));
|
||||
|
||||
List<String> options = (cacheFile != null)
|
||||
? List.of("-A" + ConfigurationMetadataAnnotationProcessor.DESCRIPTION_CACHE_LOCATION_OPTION + "="
|
||||
+ cacheFile.toAbsolutePath())
|
||||
: Collections.emptyList();
|
||||
|
||||
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, options, classes, sources);
|
||||
task.setProcessors(List.of(new TestConfigurationMetadataAnnotationProcessor()));
|
||||
return task.call();
|
||||
}
|
||||
}
|
||||
|
||||
private List<File> classpathFiles(String classpath) {
|
||||
return Arrays.stream(classpath.split(File.pathSeparator)).map(File::new).toList();
|
||||
}
|
||||
|
||||
private ConfigurationMetadata readJson(Path file) throws Exception {
|
||||
if (!Files.exists(file)) {
|
||||
return null;
|
||||
}
|
||||
try (InputStream in = Files.newInputStream(file)) {
|
||||
return new JsonMarshaller().read(in);
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigurationMetadata readMetadata(Path classOutput) throws Exception {
|
||||
return readJson(classOutput.resolve(METADATA_PATH));
|
||||
}
|
||||
|
||||
private String descriptionOf(ConfigurationMetadata metadata, String propertyName) {
|
||||
if (metadata == null) {
|
||||
return null;
|
||||
}
|
||||
return metadata.getItems()
|
||||
.stream()
|
||||
.filter((item) -> item.isOfItemType(ItemMetadata.ItemType.PROPERTY))
|
||||
.filter((item) -> propertyName.equals(item.getName()))
|
||||
.findFirst()
|
||||
.map(ItemMetadata::getDescription)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private JavaFileObject inMemorySource(String className, String content) {
|
||||
URI uri = URI.create("string:///" + className.replace('.', '/') + ".java");
|
||||
return new SimpleJavaFileObject(uri, JavaFileObject.Kind.SOURCE) {
|
||||
@Override
|
||||
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
|
||||
return content;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user