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));
    }
  }
}
The program is started with three system properties, as an operations team might pass them -
java -Dshop.name="JavaBrahman Books" -Dshop.timeout=PT0.75S -Dshop.currencies=USD,EUR,INR com.javabrahman.spring.spel.placeholders.PlaceholderApp
 OUTPUT of the above code
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-size and shop.beta were not set, so the defaults after the colon were used: 20 and false.
  • Spring converted each string to the parameter's type: "20" to an int, "PT0.75S" to a Duration of 750 milliseconds, "USD,EUR,INR" to a List of three strings, and "false" to a boolean.
  • Numbers and booleans convert out of the box, but types such as Duration need a conversion service: a bean named conversionService, here a DefaultFormattingConversionService. 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 friendlier 750ms, 5s or 2h.
  • The static PropertySourcesPlaceholderConfigurer bean 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 @Value parameters.

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.
They can be combined, and placeholders are resolved first: in #{'${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));
  }
}
 OUTPUT of the above code
'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 as gt and lt, which are handy inside XML or annotations.
  • Methods and constructors: any public method can be called, and new creates objects - here a French Locale whose 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, salary means tom.getSalary() and name.length() calls a method on tom.getName(). SpEL finds properties through getter methods.
  • Safe navigation ?. returns null instead of throwing a NullPointerException when the left side is null: Tom has no manager, Harry has one.
  • The Elvis operator ?: is a short ternary that supplies a default when a value is null (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);
    }
  }
}
 OUTPUT of the above code
taxPercent     = 18.0
country        = GB
freeShipping   = true
currencyCount  = 3
sessionSeconds = 1800

Explanation of the code

  • #{pricing.taxRate * 100} named the bean pricing, read its taxRate property through getTaxRate(), and multiplied it, giving 18.0. SpEL arithmetic follows Java's rules, so a double times an int is a double.
  • In freeShipping, the placeholder ${shop.country} was resolved to GB first, and then SpEL called contains('GB') on the other bean's list.
  • systemProperties['shop.country'] read the JVM property directly, with ?: giving a fallback if it is missing.
  • sessionSeconds used the placeholder's default of 30 minutes inside a static call to Duration.ofMinutes().
  • These are field injections, which keeps the example short. In application code, the same @Value annotations 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") - #id refers to the method parameter id.
  • 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

A StandardEvaluationContext, 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());
      }
    }
  }
}
 OUTPUT of the above code
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.System through T(...) was rejected, because type references are not available in a SimpleEvaluationContext.
  • 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.