Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public void execute(Project project) {
configureBootTestRunTask(project, resolveMainTestClassName);
project.afterEvaluate(this::configureUtf8Encoding);
configureParametersCompilerArg(project);
configureAdditionalMetadataLocations(project);
configureConfigurationProcessorCompilerArgs(project);
configureSpringBootStarterTestToDependOnJUnitPlatformLauncher(project);
}

Expand Down Expand Up @@ -263,13 +263,13 @@ private void configureParametersCompilerArg(Project project) {
});
}

private void configureAdditionalMetadataLocations(Project project) {
private void configureConfigurationProcessorCompilerArgs(Project project) {
project.afterEvaluate((evaluated) -> evaluated.getTasks()
.withType(JavaCompile.class)
.configureEach(this::configureAdditionalMetadataLocations));
.configureEach(this::configureConfigurationProcessorCompilerArgs));
}

private void configureAdditionalMetadataLocations(JavaCompile compile) {
private void configureConfigurationProcessorCompilerArgs(JavaCompile compile) {
SourceSetContainer sourceSets = compile.getProject()
.getExtensions()
.getByType(JavaPluginExtension.class)
Expand All @@ -278,7 +278,12 @@ private void configureAdditionalMetadataLocations(JavaCompile compile) {
.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 ConfigurationProcessorCompilerArgsConfigurer(locations, descriptionCache));
});
}

private void configureProductionRuntimeClasspathConfiguration(Project project) {
Expand Down Expand Up @@ -342,16 +347,19 @@ private void configureSpringBootStarterTestToDependOnJUnitPlatformLauncher(Proje
}

/**
* Task {@link Action} to add additional meta-data locations. We need to use an
* inner-class rather than a lambda due to
* https://github.com/gradle/gradle/issues/5510.
* Task {@link Action} to add compiler args used by
* {@code spring-boot-configuration-processor}. We need to use an inner-class rather
* than a lambda due to https://github.com/gradle/gradle/issues/5510.
*/
private static final class AdditionalMetadataLocationsConfigurer implements Action<Task> {
private static final class ConfigurationProcessorCompilerArgsConfigurer implements Action<Task> {

private final Set<File> locations;

private AdditionalMetadataLocationsConfigurer(Set<File> locations) {
private final File descriptionCache;

private ConfigurationProcessorCompilerArgsConfigurer(Set<File> locations, File descriptionCache) {
this.locations = locations;
this.descriptionCache = descriptionCache;
}

@Override
Expand All @@ -361,6 +369,7 @@ public void execute(Task task) {
}
if (hasConfigurationProcessorOnClasspath(compile)) {
configureAdditionalMetadataLocations(compile);
configureDescriptionCacheLocation(compile);
}
}

Expand All @@ -379,6 +388,13 @@ private void configureAdditionalMetadataLocations(JavaCompile compile) {
+ StringUtils.collectionToCommaDelimitedString(this.locations));
}

private void configureDescriptionCacheLocation(JavaCompile compile) {
compile.getOptions()
.getCompilerArgs()
.add("-Aorg.springframework.boot.configurationprocessor.descriptionCacheLocation="
+ this.descriptionCache.getAbsolutePath());
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ void errorMessageIsHelpfulWhenMainClassCannotBeResolved() {
}

@TestTemplate
void additionalMetadataLocationsConfiguredWhenProcessorIsPresent() throws IOException {
void configurationPropertiesCompilerArgumentsConfiguredWhenProcessorIsPresent() throws IOException {
createMinimalMainSource();
File libs = new File(this.gradleBuild.getProjectDir(), "libs");
libs.mkdirs();
Expand All @@ -139,13 +139,19 @@ void additionalMetadataLocationsConfiguredWhenProcessorIsPresent() throws IOExce
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 additionalMetadataLocationsNotConfiguredWhenProcessorIsAbsent() throws IOException {
void configurationPropertiesCompilerArgumentsNotConfiguredWhenProcessorIsAbsent() throws IOException {
createMinimalMainSource();
BuildResult result = this.gradleBuild.build("compileJava");
BuildTask task = result.task(":compileJava");
Expand All @@ -154,6 +160,32 @@ void additionalMetadataLocationsNotConfiguredWhenProcessorIsAbsent() throws IOEx
assertThat(result.getOutput()).contains("compileJava compiler args: [-parameters]");
}

@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
void compileJavaIsUpToDateOnSecondInvocation() throws IOException {
createMinimalMainSource();
BuildResult result = this.gradleBuild.build("compileJava");
BuildTask task = result.task(":compileJava");
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
result = this.gradleBuild.build("compileJava");
task = result.task(":compileJava");
assertThat(task).isNotNull();
assertThat(task.getOutcome()).isEqualTo(TaskOutcome.UP_TO_DATE);
}

@TestTemplate
void applyingJavaPluginCreatesDevelopmentOnlyConfiguration() {
assertThat(this.gradleBuild.build("help").getOutput()).contains("developmentOnly exists = true");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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}'
}
Original file line number Diff line number Diff line change
@@ -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}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;

Expand All @@ -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;
}
Expand Down Expand Up @@ -189,6 +197,10 @@ public synchronized void init(ProcessingEnvironment env) {
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
Expand Down Expand Up @@ -289,6 +301,9 @@ private void processTypeElement(String prefix, TypeElement element, ExecutableEl
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);
Expand Down Expand Up @@ -405,14 +420,38 @@ protected void writeSourceMetadata() throws Exception {
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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}

}
Loading