DEV Community

Ankit Verma
Ankit Verma

Posted on

SpEL basics

What SpEL actually is

Spring lets you configure a lot of things with strings. You put a string in an annotation, and Spring turns it into a value at runtime. Most of the time that string is a plain constant. But sometimes you want the value to be computed — read another bean, do a little math, pick between two options — and a plain constant can't do that.

SpEL, the Spring Expression Language, is Spring's answer. It's a tiny language you write inside a string, and Spring evaluates it while it's wiring your application together. Think of it as a formula bar for your configuration: instead of a fixed value, you hand Spring a small expression, and Spring runs it to produce the value.

You meet SpEL the first time you see a #{ ... } inside an annotation. It shows up in @Value, in caching conditions, in security rules, in @ConditionalOnExpression. This article builds it up from the simplest possible expression to the parts that quietly bite people in production.

The two dollar-hash cousins

Before any SpEL, we have to clear up the single most common confusion, because the two look almost identical.

You have probably written this:

@Value("${server.port}")
private int port;
Enter fullscreen mode Exit fullscreen mode

That ${ ... } is not SpEL. It's a property placeholder — Spring looks up the key server.port in your properties (from application.properties, environment variables, and so on) and drops the value in. It only does key lookups. It can't do math or call methods.

SpEL uses a hash, not a dollar:

@Value("#{2 * 60 * 1000}")
private long cacheMillis;   // 120000
Enter fullscreen mode Exit fullscreen mode

The #{ ... } says "evaluate this as an expression." So the rule of thumb: ${} looks something up, #{} computes something. They can even work together, which we'll get to — but keep them separate in your head for now.

Your first expressions: literals and math

The cleanest way to learn SpEL is to see what goes between the #{ and }. At its simplest, an expression is just a literal value.

@Value("#{true}")          // boolean
@Value("#{'Freddie'}")     // string, single-quoted
@Value("#{3.14159}")       // double
Enter fullscreen mode Exit fullscreen mode

Strings inside SpEL use single quotes, because the whole expression already lives inside the double quotes of the Java annotation. Once you have literals, you get operators for free:

@Value("#{100 * 1024}")           // 102400  — arithmetic
@Value("#{'Spring'.length()}")    // 6        — method call on a string
@Value("#{'a,b,c'.split(',')}")   // ["a","b","c"]
Enter fullscreen mode Exit fullscreen mode

That last line is the key idea: inside an expression you can call any method on any value, exactly as you would in Java. SpEL isn't a cut-down mini-language for constants — it can genuinely run things. Which immediately raises the question: run things on what? So far we've only touched literals. The real power starts when an expression can reach the objects Spring already manages.

Reaching into other beans

Spring keeps every object it manages in a container, and each one has a name — its bean name. SpEL can name a bean directly and pull a value out of it.

Say you have a bean that holds some tuning numbers:

@Component("tuning")
public class Tuning {
    public int getPoolSize() { return 8; }
}
Enter fullscreen mode Exit fullscreen mode

Another bean can borrow that value through SpEL:

@Value("#{tuning.poolSize}")
private int poolSize;   // 8
Enter fullscreen mode Exit fullscreen mode

Read that expression left to right: tuning is the bean, and .poolSize calls its getter. SpEL turns .poolSize into a call to getPoolSize() for you — it follows the JavaBean property convention, so you write the short property name and Spring finds the getter.

You can go further and call methods with arguments, chain calls, and reach built-in objects Spring exposes, like system properties:

@Value("#{systemProperties['user.timezone']}")
private String tz;
Enter fullscreen mode Exit fullscreen mode

Here systemProperties is a Map Spring makes available, and ['user.timezone'] indexes into it — the same bracket syntax works for lists and arrays too.

Mixing properties and expressions

Now we can bring the two cousins back together. Spring resolves the property placeholder ${} first, before it hands the string to SpEL. That ordering lets you nest one inside the other: look a value up, then compute with it.

@Value("#{'${app.admins}'.split(',')}")
private String[] admins;
Enter fullscreen mode Exit fullscreen mode

Walk the order carefully. First ${app.admins} is replaced with, say, the string alice,bob. That leaves SpEL looking at #{'alice,bob'.split(',')}, which it evaluates into a two-element array. The property gives you the raw text; SpEL reshapes it. This "look it up, then transform it" pattern is most of what people use SpEL for in day-to-day config.

Operators that make config decisions

SpEL really earns its place when a value has to depend on something. It has the operators you'd expect, plus three shorthands worth knowing by name.

The ternary operator picks between two values based on a condition:

@Value("#{tuning.poolSize > 4 ? 'large' : 'small'}")
private String profile;
Enter fullscreen mode Exit fullscreen mode

The Elvis operator, ?:, is a shorter ternary for one specific job: "use this, or a fallback if it's null." It's named for the way ?: looks like Elvis's hair and eyes.

@Value("#{systemProperties['region'] ?: 'us-east'}")
private String region;   // the property, or 'us-east' if it's absent
Enter fullscreen mode Exit fullscreen mode

The safe-navigation operator, ?., stops a null from blowing up a chain. If the left side is null, the whole expression is null instead of throwing:

@Value("#{tuning?.poolSize}")
private Integer poolSize;   // null if 'tuning' were null, no exception
Enter fullscreen mode Exit fullscreen mode

Together these three let a single line of config express "compute a value, fall back gracefully, and don't crash on a missing piece" — logic you'd otherwise write in Java.

Filtering collections without a loop

This is the part of SpEL that feels like a superpower the first time you see it. Given a collection, SpEL can filter and transform it inline.

Selection filters a collection with .?[ ... ], where inside the brackets each element is available as #this:

// keep only the servers whose port is above 8000
@Value("#{servers.?[port > 8000]}")
private List<Server> highPorts;
Enter fullscreen mode Exit fullscreen mode

Projection transforms each element with .![ ... ], pulling one piece out of every item:

// turn a list of Server objects into a list of their names
@Value("#{servers.![name]}")
private List<String> serverNames;
Enter fullscreen mode Exit fullscreen mode

Read .?[] as "select where" and .![] as "map to." In two short expressions you've done a filter and a map that would each be a loop or a stream pipeline in Java. There are cousins too — .^[] grabs the first match and .$[] the last — but selection and projection are the two you'll reach for.

Running SpEL yourself

Everything so far happened inside annotations, where Spring runs the expression for you. But SpEL is a normal library, and you can drive it directly — useful when your own code needs to evaluate a formula, perhaps one a user or a config file supplied.

The entry point is an ExpressionParser. You parse a string into an Expression, then ask for its value:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Spring'.length() * 2");
int result = (int) exp.getValue();   // 12
Enter fullscreen mode Exit fullscreen mode

Often you want the expression to run against some object — evaluate it "in the context of" a particular target. You pass that target as the root object:

Tuning tuning = new Tuning();
Expression exp = parser.parseExpression("poolSize > 4");
boolean big = exp.getValue(tuning, Boolean.class);   // properties resolve against 'tuning'
Enter fullscreen mode Exit fullscreen mode

Now poolSize in the expression resolves against the tuning object you handed in. That idea — what the expression is allowed to see — is not just a convenience. It's the security boundary, and it's the last and most important thing to understand.

The context is the security boundary

When SpEL evaluates an expression, it does so inside an EvaluationContext — the object that decides what the expression can reach: which variables, which beans, whether it can even name Java types. There are two you'll encounter, and the difference matters enormously.

StandardEvaluationContext is the powerful one, and the default when Spring evaluates internally. It can do everything SpEL allows — including naming any class and calling any static method through the T() type operator:

// with a StandardEvaluationContext, this runs:
T(java.lang.Runtime).getRuntime().exec('rm -rf /')
Enter fullscreen mode Exit fullscreen mode

That is not a hypothetical. If you ever pass a user-supplied string into a StandardEvaluationContext, you have handed the user the ability to run arbitrary code inside your JVM. This exact mistake — SpEL injection — is behind a string of real, severe Spring vulnerabilities.

SimpleEvaluationContext exists precisely for that danger. It's a deliberately restricted context: it allows property access and simple operators but forbids type references and arbitrary method resolution. When an expression comes from anywhere you don't fully trust, this is the one to use:

EvaluationContext ctx = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression exp = parser.parseExpression(userSuppliedString);
Object value = exp.getValue(ctx, target);   // can read properties, cannot reach Runtime
Enter fullscreen mode Exit fullscreen mode

The rule is simple and worth burning in: your own trusted expressions can use the standard context; anything a user can influence must use SimpleEvaluationContext. SpEL is powerful because it can run real code — which means the moment untrusted input reaches it, that power is the whole problem.

Where you'll meet it again

You now have the shape of SpEL end to end: a #{} string Spring evaluates at runtime, distinct from a ${} property lookup, able to do math, call methods, reach into beans, choose values with ternary and Elvis, filter collections with selection and projection, and — when you drive it yourself — run inside a context that is either fully powered or safely fenced.

The same #{} you learned here reappears across Spring: in @Cacheable(condition = "...") to decide when to cache, in @PreAuthorize("hasRole('ADMIN')") for security rules, and in @ConditionalOnExpression to switch beans on and off. They're all the same language you just built up — so wherever you see those hash-braces next, you already know how to read them.

Top comments (0)