Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding @PropertySource and PropertySourcesPlaceholderConfigurer for Efficient Spring Configuration

Tech 1

Practical Examples

Prerequisites: A hello/hello.properties file exists in the resources directory with the following content:

hello=greeting

Example 1: In a GreetingController class, use the @PropertySource annotation to reference the properties file. Then, inject the value of the hello key using @Value.

@PropertySource({"classpath:hello/hello.properties"})
@RestController
public class GreetingController {
    @Value("${hello}")
    private String greetingMessage;

    @GetMapping("/greeting")
    public String getGreeting() {
        return greetingMessage;
    }
}

Executing this example returns the string greeting.

Example 2: In a SecondaryController class, use @PropertySource to referance the properties file. The GreetingController can still inject the hello value via @Value without its own @PropertySource annotation.

@RestController
public class GreetingController {
    @Value("${hello}")
    private String greetingMessage;

    @GetMapping("/greeting")
    public String getGreeting() {
        return greetingMessage;
    }
}

@RestController
@PropertySource({"classpath:hello/hello.properties"})
public class SecondaryController {
    // Code omitted
}

The result matches Example 1, demonstrating that once a bean references a properties file with @PropertySource, other beans can inject values from it without additional annotations.

Example 3:

@Getter
@Setter
public class SampleBean {
    private String fieldOne;
    private String fieldTwo;
}

@RestController
public class GreetingController {
    @Value("${hello}")
    private String greetingMessage;

    @Autowired
    private SampleBean sampleBean;

    @GetMapping("/greeting")
    public String getGreeting() {
        System.out.println("FieldOne = " + sampleBean.getFieldOne());
        System.out.println("FieldTwo = " + sampleBean.getFieldTwo());
        return greetingMessage;
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:sampleBean/sampleBean.properties"/>

    <bean id="sampleBean" class="com.example.SampleBean">
        <property name="fieldOne" value="${propOne}"/>
        <property name="fieldTwo" value="${propTwo}"/>
        <!-- Other configurations omitted -->
    </bean>
</beans>

The sampleBean.properties file contains:

propOne=valueOne
propTwo=valueTwo

Execution correctly populates sampleBean fields with values from sampleBean.properties.

Example 4: Add a propOne entry to hello.properties, keeping other configurations as in Example 3:

propOne=alternativeValueOne

Result: fieldOne in sampleBean uses the value from hello.properties, while fieldTwo uses the value from sampleBean.properties.

Source Code Analysis

@PropertySource Annotation

Spring provides the BeanDefinitionRegistryPostProcessor interface, which includes a method for registering additional bean definitions:

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
    void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}

ConfigurationClassPostProcessor implements this interface. Its postProcessBeanDefinitionRegistry() method uses ConfigurationClassParser to parse classes annotated with @Configuration and others, registering them as bean definitions.

In ConfigurationClassParser, the doProcessConfigurationClass() method processes all @PropertySource annotations, loading configuration files from specified paths and registering them in the Environment:

protected final SourceClass doProcessConfigurationClass(
    ConfigurationClass configClass, SourceClass sourceClass,
    Predicate<String> filter) throws IOException {
    // Process @PropertySource annotations
    for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
        sourceClass.getMetadata(), org.springframework.context.annotation.PropertySource.class,
        PropertySources.class, true)) {
        if (this.propertySourceRegistry != null) {
            this.propertySourceRegistry.processPropertySource(propertySource);
        } else {
            logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                    "]. Reason: Environment must implement ConfigurableEnvironment");
        }
    }
}

PropertySourceRegistry.processPropertySource() retrieves file loactions from the annotation and delegates to PropertySourceProcessor:

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> factoryClassToUse =
            (factoryClass != PropertySourceFactory.class ? factoryClass : null);
    PropertySourceDescriptor descriptor = new PropertySourceDescriptor(Arrays.asList(locations),
            ignoreResourceNotFound, name, factoryClassToUse, encoding);
    this.propertySourceProcessor.processPropertySource(descriptor);
    this.descriptors.add(descriptor);
}

PropertySourceProcessor.processPropertySource() iterates through each location, loads configuration files, and adds them to the Environment's propertySources:

public void processPropertySource(PropertySourceDescriptor descriptor) throws IOException {
    String name = descriptor.name();
    String encoding = descriptor.encoding();
    List<String> locations = descriptor.locations();
    boolean ignoreResourceNotFound = descriptor.ignoreResourceNotFound();
    PropertySourceFactory factory = (descriptor.propertySourceFactory() != null ?
            instantiateClass(descriptor.propertySourceFactory()) : defaultPropertySourceFactory);

    for (String location : locations) {
        try {
            String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
            for (Resource resource : this.resourcePatternResolver.getResources(resolvedLocation)) {
                addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
            }
        } catch (RuntimeException | IOException ex) {
            // Exception handling omitted
        }
    }
}

private void addPropertySource(PropertySource<?> propertySource) {
    String name = propertySource.getName();
    MutablePropertySources propertySources = this.environment.getPropertySources();

    if (this.propertySourceNames.contains(name)) {
        // Logic for duplicate names omitted
    }

    if (this.propertySourceNames.isEmpty()) {
        propertySources.addLast(propertySource);
    } else {
        String lastAdded = this.propertySourceNames.get(this.propertySourceNames.size() - 1);
        propertySources.addBefore(lastAdded, propertySource);
    }
    this.propertySourceNames.add(name);
}

In AbstractApplicationContext.finishBeanFactoryInitialization(), an EmbeddedValueResolver is registered if no BeanFactoryPostProcessor (like PropertySourcesPlaceholderConfigurer) has already done so, linking with PropertySourcesPlaceholderConfigurer:

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    if (!beanFactory.hasEmbeddedValueResolver()) {
        beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
    }
    beanFactory.preInstantiateSingletons();
}

PropertySourcesPlaceholderConfigurer

PropertySourcesPlaceholderConfigurer implements BeanFactoryPostProcessor. Its postProcessBeanFactory() method constructs a PropertySource from the environment and adds it to propertySources. Then, it creates another PropertySource from its configured location (specified in XML) and adds it, by default, to the end of propertySources. This order is crucial for understanding Example 4. Final, it builds a ConfigurablePropertyResolver and calls processProperties():

public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    if (this.propertySources == null) {
        this.propertySources = new MutablePropertySources();
        if (this.environment != null) {
            PropertyResolver propertyResolver = this.environment;
            if (this.ignoreUnresolvablePlaceholders &&
                    (this.environment instanceof ConfigurableEnvironment configurableEnvironment)) {
                PropertySourcesPropertyResolver resolver =
                        new PropertySourcesPropertyResolver(configurableEnvironment.getPropertySources());
                resolver.setIgnoreUnresolvableNestedPlaceholders(true);
                propertyResolver = resolver;
            }
            PropertyResolver propertyResolverToUse = propertyResolver;
            this.propertySources.addLast(
                new PropertySource<>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
                    @Override
                    @Nullable
                    public String getProperty(String key) {
                        return propertyResolverToUse.getProperty(key);
                    }
                }
            );
        }
        try {
            PropertySource<?> localPropertySource =
                    new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties());
            if (this.localOverride) {
                this.propertySources.addFirst(localPropertySource);
            } else {
                this.propertySources.addLast(localPropertySource);
            }
        } catch (IOException ex) {
            throw new BeanInitializationException("Could not load properties", ex);
        }
    }
    processProperties(beanFactory, createPropertyResolver(this.propertySources));
    this.appliedPropertySources = this.propertySources;
}

processProperties() constructs a StringValueResolver and calls doProcessProperties():

protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
    final ConfigurablePropertyResolver propertyResolver) throws BeansException {
    propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
    propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
    propertyResolver.setValueSeparator(this.valueSeparator);
    propertyResolver.setEscapeCharacter(this.escapeCharacter);

    StringValueResolver valueResolver = strVal -> {
        String resolved = (this.ignoreUnresolvablePlaceholders ?
                propertyResolver.resolvePlaceholders(strVal) :
                propertyResolver.resolveRequiredPlaceholders(strVal));
        if (this.trimValues) {
            resolved = resolved.trim();
        }
        return (resolved.equals(this.nullValue) ? null : resolved);
    };
    doProcessProperties(beanFactoryToProcess, valueResolver);
}

doProcessProperties() uses the StringValueResolver to create a BeanDefinitionVisitor, which resolves property references in bean definitions. It then sets the StringValueResolver in the BeanFactory via addEmbeddedValueResolver(), linking back to AbstractApplicationContext.finishBeanFactoryInitialization():

protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
    StringValueResolver valueResolver) {
    BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
    String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
    for (String curName : beanNames) {
        if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
            BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
            try {
                visitor.visitBeanDefinition(bd);
            } catch (Exception ex) {
                throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
            }
        }
    }
    beanFactoryToProcess.resolveAliases(valueResolver);
    beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
}

In DefaultListableBeanFactory.resolveEmbeddedValue(), @Value annotation resolution invokes the resolveStringValue() method of the StringValueResolver:

public String resolveEmbeddedValue(@Nullable String value) {
    if (value == null) {
        return null;
    }
    String result = value;
    for (StringValueResolver resolver : this.embeddedValueResolvers) {
        result = resolver.resolveStringValue(result);
        if (result == null) {
            return null;
        }
    }
    return result;
}

Example Explanations

For Example 2: During bean definition parsing, all @PropertySource configurations are loaded into the centralized Environment. @Value annotations retrieve values from this central source, so only one class needs @PropertySource.

For Example 3: This relies on BeanFactoryPostProcessor. Its postProcessBeanFactory() method resolves property references in bean definitions before bean instantiation.

For Example 4: By default, property resolution includes all @PropertySource files followed by PropertySourcesPlaceholderConfigurer location files, searched in order. If a key exists in an @PropertySource file, its value is returned without checking later files, explaining the override in Example 4.

Spring provides a local-override setting. When set to true, PropertySourcesPlaceholderConfigurer files take precedence by being added to the front of propertySources:

<context:property-placeholder location="classpath:sampleBean.properties" local-override="true" />
if (this.localOverride) {
    this.propertySources.addFirst(localPropertySource);
} else {
    this.propertySources.addLast(localPropertySource);
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.