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);
}
}

View File

@@ -24,6 +24,7 @@ import java.util.function.Consumer;
import javax.lang.model.element.Modifier;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.MethodReference;
@@ -46,6 +47,8 @@ import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.testfixture.context.generator.annotation.ImportAwareConfiguration;
import org.springframework.context.testfixture.context.generator.annotation.ImportConfiguration;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec;
@@ -70,84 +73,262 @@ class ConfigurationClassPostProcessorAotContributionTests {
new MockBeanFactoryInitializationCode(this.generationContext);
@Test
void processAheadOfTimeWhenNoImportAwareConfigurationReturnsNull() {
assertThat(getContribution(SimpleConfiguration.class)).isNull();
@Nested
class ImportAwareTests {
@Test
void processAheadOfTimeWhenNoImportAwareConfigurationReturnsNull() {
assertThat(getContribution(SimpleConfiguration.class)).isNull();
}
@Test
void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorWithMapEntry() {
BeanFactoryInitializationAotContribution contribution = getContribution(ImportConfiguration.class);
contribution.applyTo(generationContext, beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
DefaultListableBeanFactory freshBeanFactory = freshContext.getDefaultListableBeanFactory();
initializer.accept(freshBeanFactory);
freshContext.refresh();
assertThat(freshBeanFactory.getBeanPostProcessors()).filteredOn(ImportAwareAotBeanPostProcessor.class::isInstance)
.singleElement().satisfies(postProcessor -> assertPostProcessorEntry(postProcessor, ImportAwareConfiguration.class,
ImportConfiguration.class));
freshContext.close();
});
}
@Test
void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorAfterApplicationContextAwareProcessor() {
BeanFactoryInitializationAotContribution contribution = getContribution(TestAwareCallbackConfiguration.class);
contribution.applyTo(generationContext, beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
DefaultListableBeanFactory freshBeanFactory = freshContext.getDefaultListableBeanFactory();
initializer.accept(freshBeanFactory);
freshContext.registerBean(TestAwareCallbackBean.class);
freshContext.refresh();
TestAwareCallbackBean bean = freshContext.getBean(TestAwareCallbackBean.class);
assertThat(bean.instances).hasSize(2);
assertThat(bean.instances.get(0)).isEqualTo(freshContext);
assertThat(bean.instances.get(1)).isInstanceOfSatisfying(AnnotationMetadata.class, metadata ->
assertThat(metadata.getClassName()).isEqualTo(TestAwareCallbackConfiguration.class.getName()));
freshContext.close();
});
}
@Test
void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorBeforeRegularBeanPostProcessor() {
BeanFactoryInitializationAotContribution contribution = getContribution(
TestImportAwareBeanPostProcessorConfiguration.class);
contribution.applyTo(generationContext, beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
DefaultListableBeanFactory freshBeanFactory = freshContext.getDefaultListableBeanFactory();
initializer.accept(freshBeanFactory);
freshBeanFactory.registerBeanDefinition(TestImportAwareBeanPostProcessor.class.getName(),
new RootBeanDefinition(TestImportAwareBeanPostProcessor.class));
RootBeanDefinition bd = new RootBeanDefinition(String.class);
bd.setInstanceSupplier(() -> "test");
freshBeanFactory.registerBeanDefinition("testProcessing", bd);
freshContext.refresh();
assertThat(freshContext.getBean("testProcessing")).isInstanceOfSatisfying(AnnotationMetadata.class, metadata ->
assertThat(metadata.getClassName()).isEqualTo(TestImportAwareBeanPostProcessorConfiguration.class.getName())
);
freshContext.close();
});
}
@Test
void applyToWhenHasImportAwareConfigurationRegistersHints() {
BeanFactoryInitializationAotContribution contribution = getContribution(ImportConfiguration.class);
contribution.applyTo(generationContext, beanFactoryInitializationCode);
assertThat(generationContext.getRuntimeHints().resources().resourcePatterns())
.singleElement()
.satisfies(resourceHint -> assertThat(resourceHint.getIncludes())
.map(ResourcePatternHint::getPattern)
.containsOnly("org/springframework/context/testfixture/context/generator/annotation/"
+ "ImportConfiguration.class"));
}
@SuppressWarnings("unchecked")
private void compile(BiConsumer<Consumer<DefaultListableBeanFactory>, Compiled> result) {
MethodReference methodReference = beanFactoryInitializationCode.getInitializers().get(0);
beanFactoryInitializationCode.getTypeBuilder().set(type -> {
CodeBlock methodInvocation = methodReference.toInvokeCodeBlock(
ArgumentCodeGenerator.of(DefaultListableBeanFactory.class, "beanFactory"),
beanFactoryInitializationCode.getClassName());
type.addModifiers(Modifier.PUBLIC);
type.addSuperinterface(ParameterizedTypeName.get(Consumer.class, DefaultListableBeanFactory.class));
type.addMethod(MethodSpec.methodBuilder("accept").addModifiers(Modifier.PUBLIC)
.addParameter(DefaultListableBeanFactory.class, "beanFactory")
.addStatement(methodInvocation)
.build());
});
generationContext.writeGeneratedContent();
TestCompiler.forSystem().withFiles(generationContext.getGeneratedFiles()).compile(compiled ->
result.accept(compiled.getInstance(Consumer.class), compiled));
}
@Configuration(proxyBeanMethods = false)
@Import(TestAwareCallbackBean.class)
static class TestAwareCallbackConfiguration {
}
static class TestAwareCallbackBean implements ImportAware, ApplicationContextAware {
private final List<Object> instances = new ArrayList<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.instances.add(applicationContext);
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.instances.add(importMetadata);
}
}
@Configuration(proxyBeanMethods = false)
@Import(TestImportAwareBeanPostProcessor.class)
static class TestImportAwareBeanPostProcessorConfiguration {
}
static class TestImportAwareBeanPostProcessor implements BeanPostProcessor, ImportAware,
Ordered, InitializingBean {
private AnnotationMetadata metadata;
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.metadata = importMetadata;
}
@Nullable
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (beanName.equals("testProcessing")) {
return this.metadata;
}
return bean;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.metadata, "Metadata was not injected");
}
}
}
@Test
void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorWithMapEntry() {
BeanFactoryInitializationAotContribution contribution = getContribution(ImportConfiguration.class);
contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
DefaultListableBeanFactory freshBeanFactory = freshContext.getDefaultListableBeanFactory();
initializer.accept(freshBeanFactory);
freshContext.refresh();
assertThat(freshBeanFactory.getBeanPostProcessors()).filteredOn(ImportAwareAotBeanPostProcessor.class::isInstance)
.singleElement().satisfies(postProcessor -> assertPostProcessorEntry(postProcessor, ImportAwareConfiguration.class,
ImportConfiguration.class));
freshContext.close();
});
}
@Nested
class PropertySourceTests {
@Test
void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorAfterApplicationContextAwareProcessor() {
BeanFactoryInitializationAotContribution contribution = getContribution(TestAwareCallbackConfiguration.class);
contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
DefaultListableBeanFactory freshBeanFactory = freshContext.getDefaultListableBeanFactory();
initializer.accept(freshBeanFactory);
freshContext.registerBean(TestAwareCallbackBean.class);
freshContext.refresh();
TestAwareCallbackBean bean = freshContext.getBean(TestAwareCallbackBean.class);
assertThat(bean.instances).hasSize(2);
assertThat(bean.instances.get(0)).isEqualTo(freshContext);
assertThat(bean.instances.get(1)).isInstanceOfSatisfying(AnnotationMetadata.class, metadata ->
assertThat(metadata.getClassName()).isEqualTo(TestAwareCallbackConfiguration.class.getName()));
freshContext.close();
});
}
@Test
void applyToWhenHasPropertySourceInvokePropertySourceProcessor() {
BeanFactoryInitializationAotContribution contribution = getContribution(
PropertySourceConfiguration.class);
contribution.applyTo(generationContext, beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
ConfigurableEnvironment environment = freshContext.getEnvironment();
assertThat(environment.containsProperty("from.p1")).isFalse();
initializer.accept(freshContext);
assertThat(environment.containsProperty("from.p1")).isTrue();
assertThat(environment.getProperty("from.p1")).isEqualTo("p1Value");
freshContext.close();
});
}
@Test
void applyToWhenHasImportAwareConfigurationRegistersBeanPostProcessorBeforeRegularBeanPostProcessor() {
BeanFactoryInitializationAotContribution contribution = getContribution(
TestImportAwareBeanPostProcessorConfiguration.class);
contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
DefaultListableBeanFactory freshBeanFactory = freshContext.getDefaultListableBeanFactory();
initializer.accept(freshBeanFactory);
freshBeanFactory.registerBeanDefinition(TestImportAwareBeanPostProcessor.class.getName(),
new RootBeanDefinition(TestImportAwareBeanPostProcessor.class));
RootBeanDefinition bd = new RootBeanDefinition(String.class);
bd.setInstanceSupplier(() -> "test");
freshBeanFactory.registerBeanDefinition("testProcessing", bd);
freshContext.refresh();
assertThat(freshContext.getBean("testProcessing")).isInstanceOfSatisfying(AnnotationMetadata.class, metadata ->
assertThat(metadata.getClassName()).isEqualTo(TestImportAwareBeanPostProcessorConfiguration.class.getName())
);
freshContext.close();
});
}
@Test
void applyToWhenHasPropertySourcesInvokesPropertySourceProcessorInOrder() {
BeanFactoryInitializationAotContribution contribution = getContribution(
PropertySourceConfiguration.class, PropertySourceDependentConfiguration.class);
contribution.applyTo(generationContext, beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
ConfigurableEnvironment environment = freshContext.getEnvironment();
assertThat(environment.containsProperty("from.p1")).isFalse();
assertThat(environment.containsProperty("from.p2")).isFalse();
initializer.accept(freshContext);
assertThat(environment.containsProperty("from.p1")).isTrue();
assertThat(environment.getProperty("from.p1")).isEqualTo("p1Value");
assertThat(environment.containsProperty("from.p2")).isTrue();
assertThat(environment.getProperty("from.p2")).isEqualTo("p2Value");
freshContext.close();
});
}
@Test
void applyToWhenHasPropertySourceWithDetailsRetainsThem() {
BeanFactoryInitializationAotContribution contribution = getContribution(
PropertySourceWithDetailsConfiguration.class);
contribution.applyTo(generationContext, beanFactoryInitializationCode);
compile((initializer, compiled) -> {
GenericApplicationContext freshContext = new GenericApplicationContext();
ConfigurableEnvironment environment = freshContext.getEnvironment();
assertThat(environment.getPropertySources().get("testp1")).isNull();
initializer.accept(freshContext);
assertThat(environment.getPropertySources().get("testp1")).isNotNull();
freshContext.close();
});
}
@SuppressWarnings("unchecked")
private void compile(BiConsumer<Consumer<GenericApplicationContext>, Compiled> result) {
MethodReference methodReference = beanFactoryInitializationCode.getInitializers().get(0);
beanFactoryInitializationCode.getTypeBuilder().set(type -> {
ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator
.of(ConfigurableEnvironment.class, "applicationContext.getEnvironment()")
.and(ArgumentCodeGenerator.of(ResourceLoader.class, "applicationContext"));
CodeBlock methodInvocation = methodReference.toInvokeCodeBlock(argCodeGenerator,
beanFactoryInitializationCode.getClassName());
type.addModifiers(Modifier.PUBLIC);
type.addSuperinterface(ParameterizedTypeName.get(Consumer.class, GenericApplicationContext.class));
type.addMethod(MethodSpec.methodBuilder("accept").addModifiers(Modifier.PUBLIC)
.addParameter(GenericApplicationContext.class, "applicationContext")
.addStatement(methodInvocation)
.build());
});
generationContext.writeGeneratedContent();
TestCompiler.forSystem().withFiles(generationContext.getGeneratedFiles()).compile(compiled ->
result.accept(compiled.getInstance(Consumer.class), compiled));
}
@Configuration(proxyBeanMethods = false)
@PropertySource("classpath:org/springframework/context/annotation/p1.properties")
static class PropertySourceConfiguration {
}
@Configuration(proxyBeanMethods = false)
@PropertySource("classpath:${base.package}/p2.properties")
static class PropertySourceDependentConfiguration {
}
@Configuration(proxyBeanMethods = false)
@PropertySource(name = "testp1", value = "classpath:org/springframework/context/annotation/p1.properties",
ignoreResourceNotFound = true)
static class PropertySourceWithDetailsConfiguration {
}
@Test
void applyToWhenHasImportAwareConfigurationRegistersHints() {
BeanFactoryInitializationAotContribution contribution = getContribution(ImportConfiguration.class);
contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode);
assertThat(generationContext.getRuntimeHints().resources().resourcePatterns())
.singleElement()
.satisfies(resourceHint -> assertThat(resourceHint.getIncludes())
.map(ResourcePatternHint::getPattern)
.containsOnly("org/springframework/context/testfixture/context/generator/annotation/"
+ "ImportConfiguration.class"));
}
@Nullable
private BeanFactoryInitializationAotContribution getContribution(Class<?> type) {
private BeanFactoryInitializationAotContribution getContribution(Class<?>... types) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerBeanDefinition("configuration", new RootBeanDefinition(type));
for (Class<?> type : types) {
beanFactory.registerBeanDefinition(type.getName(), new RootBeanDefinition(type));
}
ConfigurationClassPostProcessor postProcessor = new ConfigurationClassPostProcessor();
postProcessor.postProcessBeanFactory(beanFactory);
return postProcessor.processAheadOfTime(beanFactory);
@@ -159,81 +340,4 @@ class ConfigurationClassPostProcessorAotContributionTests {
.containsExactly(entry(key.getName(), value.getName()));
}
@SuppressWarnings("unchecked")
private void compile(BiConsumer<Consumer<DefaultListableBeanFactory>, Compiled> result) {
MethodReference methodReference = this.beanFactoryInitializationCode.getInitializers().get(0);
this.beanFactoryInitializationCode.getTypeBuilder().set(type -> {
CodeBlock methodInvocation = methodReference.toInvokeCodeBlock(
ArgumentCodeGenerator.of(DefaultListableBeanFactory.class, "beanFactory"),
this.beanFactoryInitializationCode.getClassName());
type.addModifiers(Modifier.PUBLIC);
type.addSuperinterface(ParameterizedTypeName.get(Consumer.class, DefaultListableBeanFactory.class));
type.addMethod(MethodSpec.methodBuilder("accept").addModifiers(Modifier.PUBLIC)
.addParameter(DefaultListableBeanFactory.class, "beanFactory")
.addStatement(methodInvocation)
.build());
});
this.generationContext.writeGeneratedContent();
TestCompiler.forSystem().withFiles(this.generationContext.getGeneratedFiles()).compile(compiled ->
result.accept(compiled.getInstance(Consumer.class), compiled));
}
@Configuration(proxyBeanMethods = false)
@Import(TestAwareCallbackBean.class)
static class TestAwareCallbackConfiguration {
}
static class TestAwareCallbackBean implements ImportAware, ApplicationContextAware {
private final List<Object> instances = new ArrayList<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.instances.add(applicationContext);
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.instances.add(importMetadata);
}
}
@Configuration(proxyBeanMethods = false)
@Import(TestImportAwareBeanPostProcessor.class)
static class TestImportAwareBeanPostProcessorConfiguration {
}
static class TestImportAwareBeanPostProcessor implements BeanPostProcessor, ImportAware,
Ordered, InitializingBean {
private AnnotationMetadata metadata;
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.metadata = importMetadata;
}
@Nullable
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (beanName.equals("testProcessing")) {
return this.metadata;
}
return bean;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.metadata, "Metadata was not injected");
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.core.io.support;
import java.util.Arrays;
import java.util.List;
import org.springframework.core.env.PropertySource;
import org.springframework.lang.Nullable;
/**
* Describe a {@link PropertySource}.
*
* @param locations the locations to consider
* @param ignoreResourceNotFound whether to fail if a location does not exist
* @param name the name of the property source, or {@code null} to infer one
* @param propertySourceFactory the {@link PropertySourceFactory} to use, or
* {@code null} to use the default
* @param encoding the encoding, or {@code null} to use the default encoding
* @author Stephane Nicoll
* @since 6.0
*/
public record PropertySourceDescriptor(List<String> locations, boolean ignoreResourceNotFound,
@Nullable String name, @Nullable Class<? extends PropertySourceFactory> propertySourceFactory,
@Nullable String encoding) {
/**
* Create a descriptor with the specified locations.
* @param locations the locations to consider
*/
public PropertySourceDescriptor(String... locations) {
this(Arrays.asList(locations), false, null, null, null);
}
}

View File

@@ -0,0 +1,151 @@
/*
* 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.core.io.support;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Contribute {@link PropertySource property sources} to the {@link Environment}.
*
* <p>This class is stateful and merge descriptors with the same name in a
* single {@link PropertySource} rather than creating dedicated ones.
*
* @author Stephane Nicoll
* @since 6.0
* @see PropertySourceDescriptor
*/
public class PropertySourceProcessor {
private static final PropertySourceFactory DEFAULT_PROPERTY_SOURCE_FACTORY = new DefaultPropertySourceFactory();
private static final Log logger = LogFactory.getLog(PropertySourceProcessor.class);
private final ConfigurableEnvironment environment;
private final ResourceLoader resourceLoader;
private final List<String> propertySourceNames;
public PropertySourceProcessor(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.resourceLoader = resourceLoader;
this.propertySourceNames = new ArrayList<>();
}
/**
* Process the specified {@link PropertySourceDescriptor} against the
* environment managed by this instance.
* @param descriptor the descriptor to process
* @throws IOException if loading the properties failed
*/
public void processPropertySource(PropertySourceDescriptor descriptor) throws IOException {
String name = descriptor.name();
String encoding = descriptor.encoding();
List<String> locations = descriptor.locations();
Assert.isTrue(locations.size() > 0, "At least one @PropertySource(value) location is required");
boolean ignoreResourceNotFound = descriptor.ignoreResourceNotFound();
PropertySourceFactory factory = (descriptor.propertySourceFactory() != null
? instantiateClass(descriptor.propertySourceFactory())
: DEFAULT_PROPERTY_SOURCE_FACTORY);
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(org.springframework.core.env.PropertySource<?> propertySource) {
String name = propertySource.getName();
MutablePropertySources propertySources = this.environment.getPropertySources();
if (this.propertySourceNames.contains(name)) {
// We've already added a version, we need to extend it
org.springframework.core.env.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);
}
private PropertySourceFactory instantiateClass(Class<? extends PropertySourceFactory> type) {
try {
Constructor<? extends PropertySourceFactory> constructor = type.getDeclaredConstructor();
ReflectionUtils.makeAccessible(constructor);
return constructor.newInstance();
}
catch (Exception ex) {
throw new IllegalStateException("Failed to instantiate " + type, ex);
}
}
}