Spring Configuration & Profiles

@Value & the Spring Expression Language (SpEL)

18 min Lesson 5 of 13

@Value & the Spring Expression Language (SpEL)

Properties files and environment variables store your application's external configuration: database URLs, timeout values, feature flags, and dozens of other settings that differ between development, staging, and production. @Value is the primary annotation Spring uses to inject those external values directly into your beans. On top of that, Spring ships a small but powerful expression engine — the Spring Expression Language (SpEL) — that lets you compute values at injection time rather than just copying a raw string.

Basic @Value Injection

The simplest form of @Value pulls a property from application.properties (or application.yml) using a property placeholder delimited by ${...}.

# application.properties app.name=OrderService app.max-retries=3 app.timeout-ms=5000 app.debug-mode=false
import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @Service public class OrderService { @Value("${app.name}") private String appName; @Value("${app.max-retries}") private int maxRetries; @Value("${app.timeout-ms}") private long timeoutMs; @Value("${app.debug-mode}") private boolean debugMode; public void processOrder() { System.out.printf("Service: %s | retries: %d | timeout: %d ms%n", appName, maxRetries, timeoutMs); } }

Spring performs type conversion automatically: the String value "3" in the properties file becomes an int, "5000" becomes a long, and "false" becomes a boolean. This works for all primitive types, their wrappers, and a number of common value types.

Default Values

If a property is missing and no default is provided, Spring throws a BeanCreationException at startup — fast fail, not a silent null. You can supply a fallback using the colon syntax inside the placeholder:

@Value("${app.feature-flag:false}") private boolean featureEnabled; // false if property absent @Value("${app.greeting:Hello, World!}") private String greeting;
Always provide defaults for optional settings. If a property is truly required — like a database password — leave the default out deliberately. The startup failure acts as a deployment gate: your application will not start without a value you have defined as mandatory.

Injecting Lists and Arrays

A comma-separated property value can be injected directly into an array or List:

# application.properties app.allowed-origins=https://example.com,https://app.example.com,https://admin.example.com
@Value("${app.allowed-origins}") private String[] allowedOrigins; // split on comma automatically @Value("${app.allowed-origins}") private List<String> originList; // works the same way
The comma split is done by Spring's built-in ConversionService. It applies to arrays and List<T> where T is a type Spring can convert a single string to. For complex structures (maps, nested objects) prefer @ConfigurationProperties over @Value.

Introduction to SpEL — #{...}

When you need more than a raw property value, you can embed a SpEL expression inside #{...}. SpEL is evaluated at runtime by the Spring container and can reference other beans, call methods, do arithmetic, and access system properties.

// Literal expressions @Value("#{1 + 2}") private int three; // 3 @Value("#{'Hello ' + 'World'}") private String greeting; // "Hello World" // Boolean logic @Value("#{10 > 5 and 3 < 7}") private boolean result; // true

Referencing Other Beans in SpEL

SpEL can reach into other Spring-managed beans by name, calling methods or reading properties:

@Component("appConfig") public class AppConfig { public int getMaxConnections() { return 20; } public String getRegion() { return "eu-west-1"; } }
@Service public class ReportService { // Call a method on another bean @Value("#{appConfig.maxConnections}") private int maxConnections; // Combine a bean call with string concatenation @Value("#{appConfig.region + '-reports'}") private String bucketName; // "eu-west-1-reports" }

The bean name in SpEL defaults to the simple class name with a lowercase first letter unless you specify a name explicitly with @Component("appConfig").

Mixing ${...} and #{...}

You can nest property placeholders inside SpEL expressions to combine externalized configuration with runtime computation:

# application.properties pool.base-size=5 pool.multiplier=2
// Compute the pool size at startup: 5 * 2 = 10 @Value("#{${pool.base-size} * ${pool.multiplier}}") private int computedPoolSize; // Conditional: use a safe value in test mode @Value("#{environment['app.env'] == 'test' ? 1 : ${pool.base-size}}") private int activePoolSize;
Do not over-use SpEL for business logic. If your @Value expression is longer than a single line or requires multiple conditions, that logic belongs in a @PostConstruct method or a dedicated configuration class, not in an annotation. Complex SpEL expressions are hard to test, impossible to type-check at compile time, and difficult to debug.

System Properties and Environment Variables

SpEL exposes two implicit objects: systemProperties (JVM system properties from -D flags) and systemEnvironment (OS environment variables). You can access either directly:

// Read a JVM system property (-Djava.io.tmpdir=...) @Value("#{systemProperties['java.io.tmpdir']}") private String tmpDir; // Read an OS environment variable @Value("#{systemEnvironment['HOME']}") private String homeDir;

In Spring Boot it is almost always simpler to use ${MY_ENV_VAR} (property placeholder) because Boot already maps environment variables into its Environment abstraction. Reserve the SpEL systemEnvironment syntax for cases where you need to combine it with other SpEL operations.

Constructor and Method Parameter Injection

@Value is not limited to fields. You can annotate constructor parameters and method parameters, which makes the bean easier to unit-test without a Spring context:

@Service public class PaymentService { private final String apiKey; private final int timeoutMs; public PaymentService( @Value("${payment.api-key}") String apiKey, @Value("${payment.timeout-ms:3000}") int timeoutMs) { this.apiKey = apiKey; this.timeoutMs = timeoutMs; } }

With constructor injection you can simply new PaymentService("test-key", 500) in a unit test — no application context needed, no reflection magic, no mocking framework required for the configuration itself.

@Value vs @ConfigurationProperties

Knowing when to use each avoids a common architectural smell:

  • @Value — one or two scattered properties that belong to different prefixes; quick access to computed or environment values via SpEL; places where a strongly-typed binding class would be overkill.
  • @ConfigurationProperties — a cohesive group of related properties sharing a common prefix (e.g., app.mail.*). Gives you a typed POJO, IDE auto-completion, JSR-303 validation on startup, and better testability.
Treat @Value as the exception, not the rule. For any feature that has more than two or three configuration knobs, create a @ConfigurationProperties record or class. Reserve @Value for single-property injection and SpEL for the cases where you need runtime computation.

Summary

@Value("${property}") injects externalized configuration into beans with automatic type conversion and optional defaults. @Value("#{expression}") embeds a SpEL expression evaluated at container startup, giving you access to other beans, arithmetic, conditionals, and system properties. The two syntaxes compose: you can nest ${} placeholders inside #{} expressions. Prefer constructor-parameter injection over field injection for testability, and migrate to @ConfigurationProperties whenever a group of related properties grows beyond a handful of values.

ES
Edrees Salih
1 hour ago

We are still cooking the magic in the way!