Apply @PropertySource in AOT-generated context

This commit records `@PropertySource` declarations defined on
configuration classes so that these are contributed to the environment
of a context that is initialized by generated code.

Closes gh-28976
This commit is contained in:
Stephane Nicoll
2022-08-23 11:30:54 +02:00
parent fcb6baf2e9
commit 7642ac82f6
6 changed files with 655 additions and 247 deletions

View File

@@ -16,11 +16,8 @@
package org.springframework.context.annotation;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
@@ -40,7 +37,6 @@ import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -59,17 +55,11 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.DefaultPropertySourceFactory;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import org.springframework.core.io.support.ResourcePropertySource;
import org.springframework.core.io.support.PropertySourceDescriptor;
import org.springframework.core.io.support.PropertySourceProcessor;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.StandardAnnotationMetadata;
@@ -83,7 +73,6 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
/**
* Parses a {@link Configuration} class definition, populating a collection of
@@ -109,8 +98,6 @@ import org.springframework.util.StringUtils;
*/
class ConfigurationClassParser {
private static final PropertySourceFactory DEFAULT_PROPERTY_SOURCE_FACTORY = new DefaultPropertySourceFactory();
private static final Predicate<String> DEFAULT_EXCLUSION_FILTER = className ->
(className.startsWith("java.lang.annotation.") || className.startsWith("org.springframework.stereotype."));
@@ -128,6 +115,9 @@ class ConfigurationClassParser {
private final ResourceLoader resourceLoader;
@Nullable
private final PropertySourceRegistry propertySourceRegistry;
private final BeanDefinitionRegistry registry;
private final ComponentScanAnnotationParser componentScanParser;
@@ -138,8 +128,6 @@ class ConfigurationClassParser {
private final Map<String, ConfigurationClass> knownSuperclasses = new HashMap<>();
private final List<String> propertySourceNames = new ArrayList<>();
private final ImportStack importStack = new ImportStack();
private final DeferredImportSelectorHandler deferredImportSelectorHandler = new DeferredImportSelectorHandler();
@@ -159,6 +147,9 @@ class ConfigurationClassParser {
this.problemReporter = problemReporter;
this.environment = environment;
this.resourceLoader = resourceLoader;
this.propertySourceRegistry = (this.environment instanceof ConfigurableEnvironment ce
? new PropertySourceRegistry(new PropertySourceProcessor(ce, this.resourceLoader))
: null);
this.registry = registry;
this.componentScanParser = new ComponentScanAnnotationParser(
environment, resourceLoader, componentScanBeanNameGenerator, registry);
@@ -220,6 +211,10 @@ class ConfigurationClassParser {
return this.configurationClasses.keySet();
}
List<PropertySourceDescriptor> getPropertySourceDescriptors() {
return (this.propertySourceRegistry != null ? this.propertySourceRegistry.getDescriptors()
: Collections.emptyList());
}
protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter) throws IOException {
if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
@@ -275,8 +270,8 @@ class ConfigurationClassParser {
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
if (this.propertySourceRegistry != null) {
this.propertySourceRegistry.processPropertySource(propertySource);
}
else {
logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
@@ -433,85 +428,6 @@ class ConfigurationClassParser {
}
/**
* Process the given <code>@PropertySource</code> annotation metadata.
* @param propertySource metadata for the <code>@PropertySource</code> annotation found
* @throws IOException if loading a property source failed
*/
private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
String name = propertySource.getString("name");
if (!StringUtils.hasLength(name)) {
name = null;
}
String encoding = propertySource.getString("encoding");
if (!StringUtils.hasLength(encoding)) {
encoding = null;
}
String[] locations = propertySource.getStringArray("value");
Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
for (String location : locations) {
try {
String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
Resource resource = this.resourceLoader.getResource(resolvedLocation);
addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
}
catch (IllegalArgumentException | FileNotFoundException | UnknownHostException | SocketException ex) {
// Placeholders not resolvable or resource not found when trying to open it
if (ignoreResourceNotFound) {
if (logger.isInfoEnabled()) {
logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
}
}
else {
throw ex;
}
}
}
}
private void addPropertySource(PropertySource<?> propertySource) {
String name = propertySource.getName();
MutablePropertySources propertySources = ((ConfigurableEnvironment) this.environment).getPropertySources();
if (this.propertySourceNames.contains(name)) {
// We've already added a version, we need to extend it
PropertySource<?> existing = propertySources.get(name);
if (existing != null) {
PropertySource<?> newSource = (propertySource instanceof ResourcePropertySource ?
((ResourcePropertySource) propertySource).withResourceName() : propertySource);
if (existing instanceof CompositePropertySource) {
((CompositePropertySource) existing).addFirstPropertySource(newSource);
}
else {
if (existing instanceof ResourcePropertySource) {
existing = ((ResourcePropertySource) existing).withResourceName();
}
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(newSource);
composite.addPropertySource(existing);
propertySources.replace(name, composite);
}
return;
}
}
if (this.propertySourceNames.isEmpty()) {
propertySources.addLast(propertySource);
}
else {
String firstProcessed = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
propertySources.addBefore(firstProcessed, propertySource);
}
this.propertySourceNames.add(name);
}
/**
* Returns {@code @Import} class, considering all meta-annotations.
*/

View File

@@ -16,6 +16,8 @@
package org.springframework.context.annotation;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -24,6 +26,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import javax.lang.model.element.Modifier;
@@ -65,10 +68,13 @@ import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ConfigurationClassEnhancer.EnhancedConfiguration;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PropertySourceDescriptor;
import org.springframework.core.io.support.PropertySourceProcessor;
import org.springframework.core.metrics.ApplicationStartup;
import org.springframework.core.metrics.StartupStep;
import org.springframework.core.type.AnnotationMetadata;
@@ -76,11 +82,13 @@ import org.springframework.core.type.MethodMetadata;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.CodeBlock.Builder;
import org.springframework.javapoet.MethodSpec;
import org.springframework.javapoet.ParameterizedTypeName;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
/**
* {@link BeanFactoryPostProcessor} used for bootstrapping processing of
@@ -156,6 +164,9 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
@Nullable
private List<PropertySourceDescriptor> propertySourceDescriptors;
@Override
public int getOrder() {
@@ -285,7 +296,19 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
return (beanFactory.containsBean(IMPORT_REGISTRY_BEAN_NAME) ? new AotContribution(beanFactory) : null);
boolean hasPropertySourceDescriptors = !CollectionUtils.isEmpty(this.propertySourceDescriptors);
boolean hasImportRegistry = beanFactory.containsBean(IMPORT_REGISTRY_BEAN_NAME);
if (hasPropertySourceDescriptors || hasImportRegistry) {
return (generationContext, code) -> {
if (hasPropertySourceDescriptors) {
new PropertySourcesAotContribution(this.propertySourceDescriptors).applyTo(generationContext, code);
}
if (hasImportRegistry) {
new ImportAwareAotContribution(beanFactory).applyTo(generationContext, code);
}
};
}
return null;
}
/**
@@ -390,6 +413,9 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
}
// Store the PropertySourceDescriptors to contribute them Ahead-of-time if necessary
this.propertySourceDescriptors = parser.getPropertySourceDescriptors();
if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory cachingMetadataReaderFactory) {
// Clear cache in externally provided MetadataReaderFactory; this is a no-op
// for a shared cache since it'll be cleared by the ApplicationContext.
@@ -505,7 +531,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
}
private static class AotContribution implements BeanFactoryInitializationAotContribution {
private static class ImportAwareAotContribution implements BeanFactoryInitializationAotContribution {
private static final String BEAN_FACTORY_VARIABLE = BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE;
@@ -520,7 +546,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
private final ConfigurableListableBeanFactory beanFactory;
public AotContribution(ConfigurableListableBeanFactory beanFactory) {
public ImportAwareAotContribution(ConfigurableListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@@ -585,4 +611,85 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
}
private static class PropertySourcesAotContribution implements BeanFactoryInitializationAotContribution {
private static final String ENVIRONMENT_VARIABLE = "environment";
private static final String RESOURCE_LOADER_VARIABLE = "resourceLoader";
private final List<PropertySourceDescriptor> descriptors;
PropertySourcesAotContribution(List<PropertySourceDescriptor> descriptors) {
this.descriptors = descriptors;
}
@Override
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
GeneratedMethod generatedMethod = beanFactoryInitializationCode
.getMethods()
.add("processPropertySources", this::generateAddPropertySourceProcessorMethod);
beanFactoryInitializationCode
.addInitializer(generatedMethod.toMethodReference());
}
private void generateAddPropertySourceProcessorMethod(MethodSpec.Builder method) {
method.addJavadoc("Apply known @PropertySources to the environment.");
method.addModifiers(Modifier.PRIVATE);
method.addParameter(ConfigurableEnvironment.class, ENVIRONMENT_VARIABLE);
method.addParameter(ResourceLoader.class, RESOURCE_LOADER_VARIABLE);
method.addCode(generateAddPropertySourceProcessorCode());
}
private CodeBlock generateAddPropertySourceProcessorCode() {
Builder code = CodeBlock.builder();
String processorVariable = "processor";
code.addStatement("$T $L = new $T($L, $L)", PropertySourceProcessor.class,
processorVariable, PropertySourceProcessor.class, ENVIRONMENT_VARIABLE,
RESOURCE_LOADER_VARIABLE);
code.beginControlFlow("try");
for (PropertySourceDescriptor descriptor : this.descriptors) {
code.addStatement("$L.processPropertySource($L)", processorVariable,
generatePropertySourceDescriptorCode(descriptor));
}
code.nextControlFlow("catch ($T ex)", IOException.class);
code.addStatement("throw new $T(ex)", UncheckedIOException.class);
code.endControlFlow();
return code.build();
}
private CodeBlock generatePropertySourceDescriptorCode(PropertySourceDescriptor descriptor) {
CodeBlock.Builder code = CodeBlock.builder();
code.add("new $T(", PropertySourceDescriptor.class);
CodeBlock values = descriptor.locations().stream()
.map(value -> CodeBlock.of("$S", value)).collect(CodeBlock.joining(", "));
if (descriptor.name() == null && descriptor.propertySourceFactory() == null
&& descriptor.encoding() == null && !descriptor.ignoreResourceNotFound()) {
code.add("$L)", values);
}
else {
List<CodeBlock> arguments = new ArrayList<>();
arguments.add(CodeBlock.of("$T.of($L)", List.class, values));
arguments.add(CodeBlock.of("$L", descriptor.ignoreResourceNotFound()));
arguments.add(handleNull(descriptor.name(), () -> CodeBlock.of("$S", descriptor.name())));
arguments.add(handleNull(descriptor.propertySourceFactory(),
() -> CodeBlock.of("$T.class", descriptor.propertySourceFactory())));
arguments.add(handleNull(descriptor.encoding(),
() -> CodeBlock.of("$S", descriptor.encoding())));
code.add(CodeBlock.join(arguments, ", "));
code.add(")");
}
return code.build();
}
private CodeBlock handleNull(@Nullable Object value, Supplier<CodeBlock> nonNull) {
if (value == null) {
return CodeBlock.of("null");
}
else {
return nonNull.get();
}
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2002-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.context.annotation;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.support.PropertySourceDescriptor;
import org.springframework.core.io.support.PropertySourceFactory;
import org.springframework.core.io.support.PropertySourceProcessor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Registry of {@link PropertySource} processed on configuration classes.
*
* @author Stephane Nicoll
* @since 6.0
* @see PropertySourceDescriptor
*/
class PropertySourceRegistry {
private final PropertySourceProcessor propertySourceProcessor;
private final List<PropertySourceDescriptor> descriptors;
public PropertySourceRegistry(PropertySourceProcessor propertySourceProcessor) {
this.propertySourceProcessor = propertySourceProcessor;
this.descriptors = new ArrayList<>();
}
public List<PropertySourceDescriptor> getDescriptors() {
return Collections.unmodifiableList(this.descriptors);
}
/**
* Process the given <code>@PropertySource</code> annotation metadata.
* @param propertySource metadata for the <code>@PropertySource</code> annotation found
* @throws IOException if loading a property source failed
*/
void processPropertySource(AnnotationAttributes propertySource) throws IOException {
String name = propertySource.getString("name");
if (!StringUtils.hasLength(name)) {
name = null;
}
String encoding = propertySource.getString("encoding");
if (!StringUtils.hasLength(encoding)) {
encoding = null;
}
String[] locations = propertySource.getStringArray("value");
Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");
Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
Class<? extends PropertySourceFactory> factorClassToUse =
(factoryClass != PropertySourceFactory.class ? factoryClass : null);
PropertySourceDescriptor descriptor = new PropertySourceDescriptor(Arrays.asList(locations), ignoreResourceNotFound, name,
factorClassToUse, encoding);
this.propertySourceProcessor.processPropertySource(descriptor);
this.descriptors.add(descriptor);
}
}