CodeGym /Courses /Spring Core /Environment and PropertySource

Environment and PropertySource

Spring Core
Level 12, Lesson 1
Available

1. Environment and external configuration

If you have ever changed the volume on speakers, you already intuitively understand the idea of external configuration. The amplifier circuit (the code) is complex, and you do not want to resolder it every time the room gets noisy. But the volume knob (the setting) is easy to turn depending on the situation. An application works exactly the same way: the code stays stable, while the values change from one environment to another.

In Spring, Environment plays the role of this "panel with knobs." It is not "yet another magical thing," but a very pragmatic point: the container stores a set of configuration sources and knows how to answer the question "give me the value for this key." And this is where things get interesting: there can be multiple sources, values can overlap, and the result depends on the lookup order.

Environment is a Spring interface that represents the application's "configuration picture of the world." It contains properties (properties) and, yes, profiles (profiles), but we will not touch profiles today — otherwise we would drift into future topics. What matters is this: Environment lives inside ApplicationContext, and that means two nice things. First, you can access settings right after the context starts. Second, the container can "inject" Environment wherever it is needed (for example, into @Bean methods).

The simplest way to see Environment is to ask the context for it and read a property with a default:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.Environment;

public class ContextFlowApplication {
    public static void main(String[] args) {
        // Close the context automatically so resources are released correctly
        try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Environment is available from any ApplicationContext
            Environment env = ctx.getEnvironment();

            // Read a property by key, and if it is missing, use the default
            String appName = env.getProperty("contextflow.app-name", "ContextFlow");

            // This is a convenient place to check what value was actually produced after all sources
            System.out.println("appName = " + appName); // appName = ContextFlow
        }
    }
}

Notice one detail: we asked for the value by the key contextflow.app-name, but we provided the default "ContextFlow". This is a basic habit these days: if a property does not have to be set externally, it is better to have a clear default and not turn application startup into a minefield.

Another important thought: Environment is not System.getProperty() and not System.getenv(). Yes, it can read both, but its purpose is to combine different sources into one model and provide a single API.

2. PropertySource and configuration sources

Imagine that Environment is the "Application Settings" folder, and PropertySource is a separate "sheet" (or file, or tab) that contains key-value pairs. A source has a name, and it has content. One source can be JVM system properties, another can be OS environment variables, a third can be a .properties file (we will connect one in the next lecture), and a fourth can be a map assembled programmatically (sometimes very convenient for tests and demos).

In plain Spring (without Boot), you usually already have two sources by default: JVM system properties and environment variables. Let's print the list of sources and look at the order:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

public class EnvDebug {
    public static void main(String[] args) {
        try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Get the "configurable" environment to see the PropertySource list
            ConfigurableEnvironment env = ctx.getEnvironment();

            // The order in this list is the precedence: top to bottom
            env.getPropertySources()
                    .forEach(ps -> System.out.println(ps.getName())); // Print source names
        }
    }
}

The output will be roughly like this (it may differ in details, but the idea remains the same):

Source Where it lives How it is set Can it be changed from Java while running
systemProperties inside the JVM process -Dkey=value or System.setProperty() yes (but it is better not to overuse it)
systemEnvironment the OS process environment environment variables before startup basically no (and that is actually good)

That already gives us two "control knobs" without a single .properties file: we can start the application with -D... (JVM system properties) or set environment variables (environment variables). And Spring can see both sources as one system.

To avoid confusion, let's clearly fix the difference between these sources:

Source Where it lives How it is set Can it be changed from Java while running
systemProperties inside the JVM process -Dkey=value or System.setProperty() yes (but it is better not to overuse it)
systemEnvironment the OS process environment environment variables before startup basically no (and that is actually good)

In real projects, you most often set environment variables and system properties at startup, rather than "tweaking" them from code. Changing settings in code usually smells like "we are our own environment." Sometimes this is acceptable for tests/demos, but as a development style, it is questionable.

3. Lookup order for values

In Spring there is a very simple but powerful rule: when you call env.getProperty("key"), Spring goes through the PropertySource list from top to bottom and takes the first value it finds. That is the "lookup order," also known as precedence. No magic: just a list of sources and the rule "first one found wins."

Here is a diagram worth keeping in your head (it is almost like a cafeteria line: whoever is first gets the cutlet):

flowchart TD
    A[Environment] --> B["PropertySource: systemProperties"]
    A --> C["PropertySource: systemEnvironment"]
    A --> D["Other sources (we will add them later)"]
    E["getProperty('contextflow.app-name')"] --> A
    B --> F["If the key is found -> return the value"]
    C --> G["If not found above -> try here"]
    D --> H["And only then the rest"]

Let's create a situation where the key is the same, but the values are different. The easiest way in a teaching app is to set a system property directly in main() (you should not do this in real production, but for a demo it is perfect):

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class PrecedenceDemo {
    public static void main(String[] args) {
        // For the demo, set a system property manually (in production, it is usually set with -D at startup)
        System.setProperty("contextflow.app-name", "ContextFlow-Demo");

        try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Read the "final" value: it takes the source order into account
            String appName = ctx.getEnvironment().getProperty("contextflow.app-name");

            System.out.println("appName = " + appName); // appName = ContextFlow-Demo
        }
    }
}

Here, even without files and without environment variables, we saw "overriding": as soon as the property appeared in systemProperties, it became the main one.

You can add your own artificial PropertySource (for example, a map) and place it "first." This is often used in tests, but as a teaching example it also clearly shows the mechanism:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.env.MapPropertySource;

import java.util.Map;

public class CustomPropertySourceDemo {
    public static void main(String[] args) {
        // Create the context manually so we can intervene BEFORE refresh()
        var ctx = new AnnotationConfigApplicationContext();

        // addFirst = highest priority: this source is checked first
        ctx.getEnvironment().getPropertySources().addFirst(
                new MapPropertySource(
                        "demoProps", // source name (convenient for debugging)
                        Map.of("contextflow.app-name", "ContextFlow-From-Map") // keys and values
                )
        );

        // Register the configuration, then start the context
        ctx.register(AppConfig.class);
        ctx.refresh();

        // Check that the value from demoProps "won"
        System.out.println(ctx.getEnvironment().getProperty("contextflow.app-name")); // ContextFlow-From-Map

        // Don't forget to close the context since we did not create it with try-with-resources
        ctx.close();
    }
}

What matters here is not even that we "created the context a bit more complicatedly," but that you saw this: the order can be controlled, and it is exactly what determines the final result.

4. Environment API

When you first start using Environment, the most common mistake is to forget that a property may be missing. getProperty("key") will return null in that case. No tragedy yet; the tragedy starts later, when you call null.toUpperCase() and get an NPE, and then spend two hours googling "why Spring broke" (spoiler: Spring did not break, expectations did).

In practice, there are three basic techniques that cover 90% of life:

The first technique is getProperty(key, defaultValue), when the value is optional.

String appName = env.getProperty("contextflow.app-name", "ContextFlow");

The second technique is getRequiredProperty(key), when the value must be provided externally and you want fail-fast right at startup.

String auditMode = env.getRequiredProperty("contextflow.audit.mode");

The third technique is getProperty(key, targetType, defaultValue), when you want to get a number/boolean right away without manual Integer.parseInt(...). We are not going into custom conversion yet, but Spring usually knows how to convert basic types.

Integer retryLimit = env.getProperty("contextflow.notifications.retry-limit", Integer.class, 3);

And yes, this is still "configuration as strings," but it is better than keeping parsing and validation of every setting in business logic.

5. Where to read settings in ContextFlow

Now comes a bit of "architectural hygiene." It is very easy to do this: inject Environment into every service and read values in every method. That looks "flexible," but after a couple of weeks you will discover that instead of a DI application you ended up with a "service locator for strings." Plus, you will violate the idea of stateless services again: methods start depending on external global values that are not visible from the constructor as part of the contract.

The best teaching default for our course is this: settings are read at the configuration boundary (in @Configuration and @Bean), turned into simple values (strings/numbers), and then injected into the required beans. The business service remains a regular Java class: it does not know where the value came from, it simply received it in the constructor.

For example, suppose ReportOutputManager needs to know the directory for reports. Let ReportingConfig be the module config for the reporting part, which the shared AppConfig includes in the build. We have not connected contextflow.properties yet, so let's show the default and the ability to override it through the environment:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class ReportingConfig {

    @Bean
    public ReportOutputManager reportOutputManager(Environment env) {
        // Read the setting at the configuration boundary: the rest of the application gets a "ready value"
        String outputDir = env.getProperty("contextflow.report.output-dir", "build/reports");

        // Build the business object without a dependency on the Spring API
        return new ReportOutputManager(outputDir);
    }
}

And the ReportOutputManager itself stays as "boring" as possible (and that is a compliment):

public class ReportOutputManager {
    // This stores the already "resolved" configuration: a string, not Environment
    private final String outputDir;

    public ReportOutputManager(String outputDir) {
        // If needed, validation can be done here too (or above, in the config)
        this.outputDir = outputDir;
    }

    public String outputDir() {
        // A convenient getter to use the value later (for example, when writing files)
        return outputDir;
    }
}

This preserves an important principle: infrastructure knows about Spring, business logic does not. And the application is easier to test because you can create ReportOutputManager("build/reports") without any container, if you need to.

6. Overriding settings at startup

Once you understand that Environment is a set of PropertySource objects, the question "how do I change the value" becomes very practical. For today, two methods are enough: JVM system properties and environment variables.

JVM system properties (-D...)

This is the most direct and often the most convenient way for local runs and demos. If you launch a jar directly, it looks like this:

# -D... goes into systemProperties and usually has a high priority
java -Dcontextflow.app-name=ContextFlow-Demo -jar build/libs/contextflow.jar

Spring will see this in systemProperties, and env.getProperty("contextflow.app-name") will return ContextFlow-Demo.

OS environment variables

Environment variables are usually more convenient in CI/CD environments, containers, and in general anywhere where "settings should be next to the process, not inside the launch command." On Linux/macOS, it might look like this:

# The environment variable goes into systemEnvironment
export CONTEXTFLOW_APP_NAME=ContextFlow-From-Env
java -jar build/libs/contextflow.jar

An important nuance that often confuses beginners: our key is contextflow.app-name, while the env var looks like CONTEXTFLOW_APP_NAME. Spring (even without Boot!) knows how to map such names for the system environment. But treat this as a "nice bonus," not as a reason to hold a contest for inventing the most exotic keys.

Source priority

By default, in Spring's standard environment, JVM system properties have higher priority than environment variables. So if you set both:

CONTEXTFLOW_APP_NAME=FromEnv
-Dcontextflow.app-name=FromJvm

then the final value will be FromJvm.

If you want to explain this to yourself in one sentence, let it be this: "what is closer to the JVM and more explicit for startup usually has higher priority."

7. Typical mistakes when working with Environment and PropertySource

Mistake #1: thinking that Environment is only OS environment variables.
The name really provokes it: "Environment, so env vars?" But in Spring it is broader: Environment is an aggregator of different sources. If you remember that systemProperties and systemEnvironment are two different layers, many mysterious situations stop being mysterious.

Mistake #2: reading properties directly through System.getProperty()/System.getenv(), bypassing Spring.
That code works, but you lose the main value: a single lookup order and a single place where values can be overridden. As a result, some settings live in Spring, some outside Spring, and you get a "quest to find the truth."

Mistake #3: calling env.getProperty() in every business method "for fun."
This often looks convenient, but in practice it turns business code into something dependent on global state, hard to test and hard to read. It is much clearer to read the configuration once at the boundary (in @Configuration) and pass ready-made values into the constructor.

Mistake #4: forgetting about null and defaults.
getProperty() can return null, and that is normal behavior: the property may simply be missing. What is not normal is ignoring that and then hitting an NPE somewhere unexpected. If the value is optional, provide a default. If it is required, use getRequiredProperty() and fail at startup, not "somewhere later."

Mistake #5: masking a required setting with an "empty default."
Sometimes a developer is afraid of application failure and writes env.getProperty("contextflow.audit.mode", ""). As a result, the application starts, but it lives in a half-broken state, and the bug appears later and costs more. For required things, it is better to fail honestly at startup and immediately say: "the key is missing."

1
Task
Spring Core, level 12, lesson 1
Locked
Reading a system property through Environment
Reading a system property through Environment
1
Task
Spring Core, level 12, lesson 1
Locked
Priority of MapPropertySource over the system property
Priority of MapPropertySource over the system property
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION