diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index 90aa28fdd6..ddc7d8c9bd 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java @@ -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 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 knownSuperclasses = new HashMap<>(); - private final List 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 getPropertySourceDescriptors() { + return (this.propertySourceRegistry != null ? this.propertySourceRegistry.getDescriptors() + : Collections.emptyList()); + } protected void processConfigurationClass(ConfigurationClass configClass, Predicate 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 @PropertySource annotation metadata. - * @param propertySource metadata for the @PropertySource 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 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. */ diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index 02db1a37c9..e2a8a53ad4 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -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 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 descriptors; + + PropertySourcesAotContribution(List 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 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 nonNull) { + if (value == null) { + return CodeBlock.of("null"); + } + else { + return nonNull.get(); + } + } + + } + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/PropertySourceRegistry.java b/spring-context/src/main/java/org/springframework/context/annotation/PropertySourceRegistry.java new file mode 100644 index 0000000000..8880d4c469 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/PropertySourceRegistry.java @@ -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 descriptors; + + public PropertySourceRegistry(PropertySourceProcessor propertySourceProcessor) { + this.propertySourceProcessor = propertySourceProcessor; + this.descriptors = new ArrayList<>(); + } + + public List getDescriptors() { + return Collections.unmodifiableList(this.descriptors); + } + + /** + * Process the given @PropertySource annotation metadata. + * @param propertySource metadata for the @PropertySource 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 factoryClass = propertySource.getClass("factory"); + Class 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); + } + +} diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java index 29961c6107..1c772b6154 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java @@ -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, 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 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, 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, 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 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"); - } - - } - } diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java new file mode 100644 index 0000000000..2f0b13b3cc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java @@ -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 locations, boolean ignoreResourceNotFound, + @Nullable String name, @Nullable Class 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); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java new file mode 100644 index 0000000000..58835cc229 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java @@ -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}. + * + *

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 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 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 type) { + try { + Constructor constructor = type.getDeclaredConstructor(); + ReflectionUtils.makeAccessible(constructor); + return constructor.newInstance(); + } + catch (Exception ex) { + throw new IllegalStateException("Failed to instantiate " + type, ex); + } + } + +}