Spring Expression Language (SpEL) and @Value Explained
Introduction
This tutorial explains how Spring beans get configuration values with the@Value annotation, and how the Spring Expression Language (SpEL) lets those values be computed. We will start with @Value and property placeholders - ${...} - including default values and automatic type conversion. We will then see the difference between a placeholder and a SpEL expression - #{...} - and learn the SpEL language itself by evaluating expressions directly in Java: operators, method calls, the safe-navigation and Elvis operators, type references, and selecting and projecting collections. Next we will use SpEL inside @Value to refer to other beans and to combine expressions with placeholders, look at where else Spring uses SpEL, and lastly see how to evaluate expressions safely when they come from outside the application.(This tutorial completes the Spring Core module. It assumes you know how beans are declaredClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration and how they are injectedClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field.)
@Value and property placeholders
Hard-coding settings such as a URL, a timeout or a page size into a class means rebuilding the application to change them. Spring instead reads them from the environment - JVM system properties, environment variables, property files - and@Value("${name}") injects one of them into a constructor parameter, a field or a setter. The text inside ${...} is a placeholder: the name of a property, optionally followed by a colon and a default value -
Injecting properties with @Value placeholders, defaults and type conversion
//PlaceholderApp.java
package com.javabrahman.spring.spel.placeholders;
import java.time.Duration;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.stereotype.Component;
@Component
class ShopSettings {
private final String name;
private final int pageSize;
private final Duration timeout;
private final List<String> currencies;
private final boolean betaFeatures;
ShopSettings(@Value("${shop.name}") String name,
@Value("${shop.page-size:20}") int pageSize, // default 20 if not set
@Value("${shop.timeout:PT5S}") Duration timeout, // ISO-8601 text converted to a Duration
@Value("${shop.currencies}") List<String> currencies, // comma-separated -> List
@Value("${shop.beta:false}") boolean betaFeatures) {
this.name = name;
this.pageSize = pageSize;
this.timeout = timeout;
this.currencies = currencies;
this.betaFeatures = betaFeatures;
}
@Override
public String toString() {
return "name=" + name + "\npageSize=" + pageSize + "\ntimeout=" + timeout
+ " (" + timeout.toMillis() + " ms)\ncurrencies=" + currencies + "\nbetaFeatures=" + betaFeatures;
}
}
@Configuration
@ComponentScan
class AppConfig {
// Resolves ${...} placeholders strictly: a missing property without a default fails at startup
@Bean
static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
// Must be named "conversionService": converts text to Duration, List and other types
@Bean
ConversionService conversionService() {
return new DefaultFormattingConversionService();
}
}
public class PlaceholderApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
System.out.println(context.getBean(ShopSettings.class));
}
}
}
java -Dshop.name="JavaBrahman Books" -Dshop.timeout=PT0.75S -Dshop.currencies=USD,EUR,INR com.javabrahman.spring.spel.placeholders.PlaceholderApp
name=JavaBrahman Books pageSize=20 timeout=PT0.75S (750 ms) currencies=[USD, EUR, INR] betaFeatures=false
Explanation of the code
${shop.name}was replaced by the system property's value. The same placeholder would find an environment variable or an entry in a property file, depending on which property sources the environment has.shop.page-sizeandshop.betawere not set, so the defaults after the colon were used:20andfalse.- Spring converted each string to the parameter's type:
"20"to anint,"PT0.75S"to aDurationof 750 milliseconds,"USD,EUR,INR"to aListof three strings, and"false"to aboolean. - Numbers and booleans convert out of the box, but types such as
Durationneed a conversion service: a bean namedconversionService, here aDefaultFormattingConversionService. Without it, startup fails with Cannot convert value of type 'java.lang.String' to required type 'java.time.Duration'. The plain Spring Framework reads durations in the ISO-8601 format (PT0.75S,PT5S); Spring Boot registers its own conversion service, which also accepts the friendlier750ms,5sor2h. - The
staticPropertySourcesPlaceholderConfigurerbean makes placeholder resolution strict, so a typo in a property name stops the application at startup instead of injecting the literal text${shop.nmae}. Spring Boot registers one for you. - For a whole group of related settings, Spring Boot's
@ConfigurationProperties, covered in module 2, binds them to one type-safe class and is usually the better choice than many@Valueparameters.
Placeholders vs expressions: ${...} and #{...}
@Value accepts two different syntaxes, and they are easy to confuse -
${shop.name}is a property placeholder. It is a plain lookup: Spring replaces it with the value of a property. There is no logic in it, apart from a default value.#{2 * 60}is a SpEL expression. Spring evaluates it, like a small piece of code: it can do arithmetic, call methods, refer to other beans and make decisions.
#{'${shop.currencies}'.split(',').length}, Spring first replaces the placeholder with USD,EUR,INR, then evaluates the expression 'USD,EUR,INR'.split(',').length, giving 3.
The SpEL language
SpEL is not limited to@Value; it is a general expression language with its own API. Evaluating expressions directly with a SpelExpressionParser is the quickest way to learn the syntax. The example evaluates a series of expressions, some against an Employee object that serves as the root object - the object whose properties the expression can name directly -
Evaluating SpEL expressions with SpelExpressionParser
//SpelBasicsApp.java
package com.javabrahman.spring.spel.basics;
import java.util.List;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class SpelBasicsApp {
public static class Employee {
private final String name;
private final int age;
private final double salary;
private final String manager; // null for the boss
Employee(String name, int age, double salary, String manager) {
this.name = name;
this.age = age;
this.salary = salary;
this.manager = manager;
}
public String getName() { return name; }
public int getAge() { return age; }
public double getSalary() { return salary; }
public String getManager() { return manager; }
}
public static class Team {
private final List<Employee> members = List.of(
new Employee("Tom Jones", 45, 7000, null),
new Employee("Harry Major", 25, 4000, "Tom Jones"),
new Employee("Ethan Hardy", 65, 5500, "Tom Jones"),
new Employee("Nancy Smith", 22, 3500, "Harry Major"));
public List<Employee> getMembers() { return members; }
}
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
Employee tom = new Team().getMembers().get(0);
Employee harry = new Team().getMembers().get(1);
String[] general = {
"'Hello' + ' ' + 'SpEL'", // string concatenation
"'javabrahman'.toUpperCase()", // method call on a literal
"2 * 60 + 5", // arithmetic
"10 > 3 and 'a' == 'b'", // relational and logical operators
"T(java.lang.Math).max(17, 42)", // static method through a type reference
"new java.util.Locale('fr').getDisplayLanguage(new java.util.Locale('en'))",
};
for (String expression : general) {
System.out.println(expression + "\n = " + parser.parseExpression(expression).getValue());
}
var onTom = new StandardEvaluationContext(tom);
var onHarry = new StandardEvaluationContext(harry);
System.out.println("\nRoot object Tom: name.length() = " + parser.parseExpression("name.length()").getValue(onTom));
System.out.println("Root object Tom: salary * 12 = " + parser.parseExpression("salary * 12").getValue(onTom));
System.out.println("Root object Tom: age >= 40 ? 'senior' : 'junior' = "
+ parser.parseExpression("age >= 40 ? 'senior' : 'junior'").getValue(onTom));
System.out.println("Safe navigation, Tom: manager?.toUpperCase() = "
+ parser.parseExpression("manager?.toUpperCase()").getValue(onTom));
System.out.println("Safe navigation, Harry: manager?.toUpperCase() = "
+ parser.parseExpression("manager?.toUpperCase()").getValue(onHarry));
System.out.println("Elvis, Tom: manager ?: 'reports to the board' = "
+ parser.parseExpression("manager ?: 'reports to the board'").getValue(onTom));
var onTeam = new StandardEvaluationContext(new Team());
System.out.println("\nSelection members.?[age < 30].![name] = "
+ parser.parseExpression("members.?[age < 30].![name]").getValue(onTeam));
System.out.println("Projection members.![salary] = " + parser.parseExpression("members.![salary]").getValue(onTeam));
System.out.println("First match members.^[salary > 5000].name = "
+ parser.parseExpression("members.^[salary > 5000].name").getValue(onTeam));
System.out.println("Indexing members[3].name = " + parser.parseExpression("members[3].name").getValue(onTeam));
}
}
'Hello' + ' ' + 'SpEL'
= Hello SpEL
'javabrahman'.toUpperCase()
= JAVABRAHMAN
2 * 60 + 5
= 125
10 > 3 and 'a' == 'b'
= false
T(java.lang.Math).max(17, 42)
= 42
new java.util.Locale('fr').getDisplayLanguage(new java.util.Locale('en'))
= French
Root object Tom: name.length() = 9
Root object Tom: salary * 12 = 84000.0
Root object Tom: age >= 40 ? 'senior' : 'junior' = senior
Safe navigation, Tom: manager?.toUpperCase() = null
Safe navigation, Harry: manager?.toUpperCase() = TOM JONES
Elvis, Tom: manager ?: 'reports to the board' = reports to the board
Selection members.?[age < 30].![name] = [Harry Major, Nancy Smith]
Projection members.![salary] = [7000.0, 4000.0, 5500.0, 3500.0]
First match members.^[salary > 5000].name = Tom Jones
Indexing members[3].name = Nancy Smith
Explanation of the code
- Literals and operators: strings use single quotes; SpEL has the usual arithmetic and comparison operators, plus word forms of logical operators -
and,or,not- and of comparisons such asgtandlt, which are handy inside XML or annotations. - Methods and constructors: any public method can be called, and
newcreates objects - here a FrenchLocalewhose language name is printed in English. - Type references:
T(java.lang.Math)refers to a class, so its static methods and constants can be used. - Properties of the root object: with Tom as the root object,
salarymeanstom.getSalary()andname.length()calls a method ontom.getName(). SpEL finds properties through getter methods. - Safe navigation
?.returnsnullinstead of throwing aNullPointerExceptionwhen the left side isnull: Tom has no manager, Harry has one. - The Elvis operator
?:is a short ternary that supplies a default when a value isnull(or, for a string, empty). - Collections:
.?[condition]selects the matching elements,.![expression]projects each element to a value,.^[condition]takes the first match, and[index]indexes into a list. Selection and projection can be chained, like a filter and a map in the Streams APIClick to read: Streams API.
SpEL inside @Value
In a bean definition, SpEL expressions go between#{ and }. Inside them, Spring makes every bean available by name, and adds systemProperties and systemEnvironment for the JVM's properties and the process's environment variables -
SpEL expressions in @Value referring to other beans, properties and placeholders
//ValueSpelApp.java
package com.javabrahman.spring.spel.value;
import java.util.List;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.stereotype.Component;
@Component("pricing")
class PricingRules {
public double getTaxRate() {
return 0.18;
}
public List<String> getFreeShippingCountries() {
return List.of("IN", "GB", "US");
}
}
@Component
class InvoiceSettings {
@Value("#{pricing.taxRate * 100}") // a property of another bean
double taxPercent;
@Value("#{pricing.freeShippingCountries.contains('${shop.country}')}") // placeholder inside SpEL
boolean freeShipping;
@Value("#{systemProperties['shop.country'] ?: 'unknown'}") // a system property, with Elvis
String country;
@Value("#{'${shop.currencies}'.split(',').length}") // computed from a placeholder
int currencyCount;
@Value("#{T(java.time.Duration).ofMinutes(${shop.session-minutes:30}).toSeconds()}")
long sessionSeconds;
}
@Configuration
@ComponentScan
class AppConfig {
@Bean
static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
public class ValueSpelApp {
public static void main(String[] args) {
try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
InvoiceSettings s = context.getBean(InvoiceSettings.class);
System.out.println("taxPercent = " + s.taxPercent);
System.out.println("country = " + s.country);
System.out.println("freeShipping = " + s.freeShipping);
System.out.println("currencyCount = " + s.currencyCount);
System.out.println("sessionSeconds = " + s.sessionSeconds);
}
}
}
taxPercent = 18.0 country = GB freeShipping = true currencyCount = 3 sessionSeconds = 1800
Explanation of the code
#{pricing.taxRate * 100}named the beanpricing, read itstaxRateproperty throughgetTaxRate(), and multiplied it, giving18.0. SpEL arithmetic follows Java's rules, so adoubletimes anintis adouble.- In
freeShipping, the placeholder${shop.country}was resolved toGBfirst, and then SpEL calledcontains('GB')on the other bean's list. systemProperties['shop.country']read the JVM property directly, with?:giving a fallback if it is missing.sessionSecondsused the placeholder's default of 30 minutes inside a static call toDuration.ofMinutes().- These are field injections, which keeps the example short. In application code, the same
@Valueannotations work on constructor parameters, as in the first example.
Where else Spring uses SpEL
Once you know the syntax you will recognize it across the Spring ecosystem, usually in annotation attributes where a value depends on the method's arguments or the current situation -- Caching:
@Cacheable(value = "employees", key = "#id", condition = "#id > 0")-#idrefers to the method parameterid. - Security:
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.name"). - Events:
@EventListener(condition = "#event.amount > 1000")- the listener runs only for large orders. - Scheduling and messaging: some attributes, such as Kafka listener topics, accept expressions as well as placeholders.
- Spring Data: repository query annotations can refer to method parameters and other values with SpEL.
Evaluating untrusted expressions safely
AStandardEvaluationContext, used in the examples above, gives an expression the full power of Java: it can call any method, create any object and reach any class through T(...). That is fine for expressions written by developers in annotations. It is dangerous for text that comes from users - a search filter, a formula in a spreadsheet-like feature - because an expression such as T(java.lang.Runtime).getRuntime().exec('...') would run a program on your server. For such input, use a SimpleEvaluationContext, which allows reading properties and little else -
SimpleEvaluationContext for expressions that come from users
//SafeSpelApp.java
package com.javabrahman.spring.spel.safe;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
public class SafeSpelApp {
public static class Order {
public double getAmount() { return 1200.0; }
public String getCountry() { return "IN"; }
}
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
var context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Order order = new Order();
// Expressions typed in by a user, for example as discount rules
String[] userRules = {
"amount > 1000 and country == 'IN'",
"T(java.lang.System).getProperty('user.home')",
};
for (String rule : userRules) {
try {
System.out.println(rule + "\n = " + parser.parseExpression(rule).getValue(context, order));
} catch (SpelEvaluationException e) {
System.out.println(rule + "\n rejected: " + e.getMessage());
}
}
}
}
amount > 1000 and country == 'IN'
= true
T(java.lang.System).getProperty('user.home')
rejected: EL1005E: Type cannot be found 'java.lang.System'
Explanation of the code
SimpleEvaluationContext.forReadOnlyDataBinding()builds a context that can read properties of the root object but cannot write them, call arbitrary methods or use type references.- The harmless rule - a condition on the order's amount and country - was evaluated normally.
- The rule that tried to reach
java.lang.SystemthroughT(...)was rejected, because type references are not available in aSimpleEvaluationContext. - The best defence is still not to evaluate user text as code at all. When a feature truly needs user-defined expressions, use
SimpleEvaluationContext, keep the root object small, and validate the input.
Summary
In this tutorial we first injected configuration with@Value and ${...} placeholders, using default values and letting Spring convert strings to numbers, durations, lists and booleans, with strict resolution from a PropertySourcesPlaceholderConfigurer. We then contrasted placeholders with #{...} SpEL expressions, and learned the SpEL language by evaluating expressions directly: operators, method calls, type references, root-object properties, safe navigation, the Elvis operator, and collection selection and projection. Next we used SpEL inside @Value to read other beans and system properties and to combine expressions with placeholders, and saw where else Spring uses SpEL. Lastly, we evaluated user-supplied expressions safely with a SimpleEvaluationContext. This completes the Spring Core module; the series continues with Spring Boot, starting from your first application with Spring Initializr.Spring Tutorials
Spring @Qualifier, @Primary and Conditional BeansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring Dependency Injection: Constructor vs Setter vs FieldClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field Spring Bean Scopes: Singleton, Prototype, Request, SessionClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session Spring Bean Lifecycle: @PostConstruct, @PreDestroy and MoreClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More
Spring @Qualifier, @Primary and Conditional BeansClick to read tutorial: Spring @Qualifier, @Primary and Conditional Beans What is Spring? Spring Framework vs Spring Boot ExplainedClick to read tutorial: What is Spring? Spring Framework vs Spring Boot Explained Spring IoC Container and ApplicationContext ExplainedClick to read tutorial: Spring IoC Container and ApplicationContext Explained Spring Beans: @Component, @Bean and @ConfigurationClick to read tutorial: Spring Beans: @Component, @Bean and @Configuration Spring Dependency Injection: Constructor vs Setter vs FieldClick to read tutorial: Spring Dependency Injection: Constructor vs Setter vs Field Spring Bean Scopes: Singleton, Prototype, Request, SessionClick to read tutorial: Spring Bean Scopes: Singleton, Prototype, Request, Session Spring Bean Lifecycle: @PostConstruct, @PreDestroy and MoreClick to read tutorial: Spring Bean Lifecycle: @PostConstruct, @PreDestroy and More