SpEL
SpEL, the Spring Expression Language, is a small language for expressions written as text, such as price * 2 or #order.owner == principal.username.
Why
Some values cannot be written as a constant in an annotation: a limit computed from a setting, a cache key made of one argument, a security rule that compares an argument with the current user. Dart annotations take only constants, so the framework accepts these values as text and evaluates them when they are needed.
Use SpEL where the framework asks for it. In your own code, use it for rules or templates that are kept outside the code, such as a pricing rule in configuration. For everything else, plain Dart is shorter and checked by the compiler.
Dart has no reflection in Flutter and AOT builds. An expression can read the properties and call the methods of a class only when the class is @Introspected() (metadata written by the generator) or when an accessor is registered for it.
Where the framework evaluates SpEL
| Where | Form | Sees |
|---|---|---|
@Value |
#{...} inside the text, after ${...} placeholders are replaced |
beans by name (#{mailer}), @bean, environment, systemEnvironment, systemProperties |
@ConditionalOnExpression |
the expression, with or without #{...} |
the same as @Value |
@Cacheable, @CachePut, @CacheEvict (key, condition, unless) |
plain SpEL, no #{...} |
arguments by name (#isbn) and position (#p0, #a0), #root.methodName, #root.args, #result (only in unless and @CachePut) |
@PreAuthorize, @PostAuthorize, @PreFilter, @PostFilter |
plain SpEL that gives a bool |
hasRole(...), hasAuthority(...), principal, authentication, the arguments, returnObject, filterObject, @bean |
@Query of a repository |
:#{...} for a bound value, #{#entityName} for the table |
the arguments (#book.title), beans (@clock) |
@EventListener(condition:) does not take SpEL: the condition is a Dart function, (event) => event.total >= 100. Config files never evaluate SpEL either.
import 'package:spring_core_dart/spring_core_dart.dart';
@Service()
class Basket {
Basket(
// A placeholder inside an expression, then arithmetic.
@Value(r'#{${shop.max-basket-size} * 2}') this.limit,
// Another bean, read by its name; ShopProperties is introspected.
@Value('#{shopProperties.name.toUpperCase()}') this.banner,
// A property of the Environment, with a default.
@Value("#{environment.getProperty('shop.currency', 'EUR')}")
this.currency,
);
final int limit;
final String banner;
final String currency;
}
Write the text as a raw string (r'...') when it contains ${...}. In a normal Dart string $ starts Dart's own interpolation.
Syntax
The syntax is Spring's. Names without a prefix are read on the root object.
| Kind | Examples |
|---|---|
| Literals | 'It''s', 42, 0x7F, 42L, 3.14, 1e3, true, null |
| Inline lists and maps | {1, 2, 3}, {}, {name: 'Ada', 'year': 1815}, {:} |
| Properties | name, user.address.city, 'abc'.length, {1, 2}.size |
| Indexing | items[0], scores['ada'], 'text'[0], obj['name'] |
| Method calls | name.toUpperCase(), 'a,b'.split(',').size(), @shop.isOpen() |
| Arithmetic | + - * / % ^, div, mod; ++ and -- |
| Comparison | == != < <= > >=, eq ne lt le gt ge, between {1, 5} |
| Logic | and, or, not, &&, ||, ! |
| Regular expressions | code matches '[A-Z]{3}' |
| Types | T(Math).max(1, 2), new Point(1, 2), x instanceof T(int) |
| Ternary and Elvis | stock > 0 ? 'in' : 'out', name ?: 'Unknown' |
| Safe navigation | user?.address?.city, items?.[0] |
| Selection | books.?[price > 10] (all), books.^[price > 10] (first), books.$[price > 10] (last) |
| Projection | books.![title] |
| Variables | #name, #this (the current element), #root (the root object) |
| Functions | #reverse('abc') |
| Beans | @mailer, @'dotted.name', &factoryBean |
| Assignment | name = 'Ada', #count = 1 |
| Templates | 'Total: #{price * 2}' with ParserContext.template |
Some rules worth knowing:
- Arithmetic follows Java.
7 / 2is3.%keeps the sign of the left operand. An integer division by zero throws. +concatenates when either side is a string:'n=' + 1isn=1.- Numbers compare by value across
intanddouble(2 == 2.0). Lists, sets and maps compare deeply. - Elvis (
?:) also replaces an empty string, not onlynull. - Selection and projection work on lists, sets and maps. On a map,
#thisis aMapEntrywithkeyandvalue. - A template that is a single
#{...}keeps the type of its value:'#{2}'gives theint2. Text around it makes aString.
Evaluating expressions in code
Parse once, keep the Expression, and evaluate it as often as needed. The expression holds no state; the values come from the evaluation context and the root object of each call.
import 'package:spring_core_dart/spring_core_dart.dart';
final parser = SpelExpressionParser();
// A pricing rule kept in configuration, parsed once.
final rule = parser.parseExpression('qty >= 10 ? price * 0.9 : price');
double priceOf(Map<String, Object?> line) =>
rule.getValueAs<double>(null, line); // the line is the root object
// A message template: text with #{...} parts.
final message = parser.parseExpression(
'Hello #{name}, you have #{#count} items',
ParserContext.template,
);
final context = StandardEvaluationContext({'name': 'Ada'})
..setVariable('count', 3)
..registerFunction('shout', (String s) => s.toUpperCase());
message.getValue(context); // 'Hello Ada, you have 3 items'
parser.parseExpression("#shout(name)").getValue(context); // 'ADA'
| Type | Role |
|---|---|
SpelExpressionParser |
Parses text. parseExpression(text, [ParserContext]); parseRaw(text) returns a SpelExpression whose tree (ast) can be inspected. |
ParserContext.template, TemplateParserContext('[[', ']]') |
Parse the text as a template, with #{...} or other delimiters. |
Expression |
getValue, getValueAs<T> (converts with the context's TypeConverter), setValue, isWritable, getValueType. |
StandardEvaluationContext |
For expressions you wrote. Root object, variables, functions, types, bean references, assignment. |
SimpleEvaluationContext |
For expressions from outside. Only property access; no T(...), new, @bean or variable assignment. |
SpelParserConfiguration |
Limits (expression length, nesting depth, operations per evaluation) and auto-grow of lists and null references. |
SpelParseException, SpelEvaluationException |
Errors, with a messageCode such as SpelMessage.typeNotFound (EL1005E). |
Text from users
A StandardEvaluationContext lets an expression call methods, create objects and reach beans. Never evaluate text from users with it. Use a SimpleEvaluationContext:
// A filter typed by a user: it may read, never change.
final context = SimpleEvaluationContext.forReadOnlyDataBinding()
.withRootObject(book)
.build();
parser.parseExpression('price < 20').getValue(context); // reads
parser.parseExpression("title = 'x'").getValue(context); // throws (notAssignable)
Methods stay off until withInstanceMethods() or withMethodResolvers([...]). The parser limits also protect against hostile text. The defaults are 10 000 characters, a nesting depth of 1 000 and 10 000 operations per evaluation:
final parser = SpelExpressionParser(
SpelParserConfiguration.builder()
.maximumExpressionLength(500)
.maximumOperations(2000)
.build(),
);
A matches pattern that could take exponential time, such as (a+)+, is refused with EL1073E before it runs.
What an expression can reach
Both contexts read the entries of maps as properties, the properties and methods of @Introspected classes, and the common properties and methods of core Dart types (length, size(), toUpperCase(), year, ...). For anything else, register a plug-in on a StandardEvaluationContext:
| Need | Plug-in |
|---|---|
Properties of a class that is not @Introspected |
FunctionPropertyAccessor<T>(getters: {...}, setters: {...}), or your own PropertyAccessor |
| Methods of such a class | FunctionMethodResolver<T>({...}), or your own MethodResolver |
T(Name) and new Name(...) |
SpelType.of<T>('Name', staticFields:, staticMethods:, constructors:) registered in a StandardTypeLocator |
target[index] on your own type |
an IndexAccessor |
@name |
a BeanResolver, such as BeanFactoryResolver(beanFactory) |
| A variable number of arguments | VarargsFunction(...) around the function |
// Rules may create points and read their coordinates.
final context = StandardEvaluationContext()
..typeLocator = (StandardTypeLocator()
..registerType(SpelType.of<Point>('Point', constructors: {'': Point.new})))
..addPropertyAccessor(FunctionPropertyAccessor<Point>(
getters: {'x': (p) => p.x, 'y': (p) => p.y},
))
..setVariable('p', const Point(1, 2));
parser.parseExpression('new Point(3, 4)').getValue(context); // a Point
parser.parseExpression('#p.x + #p.y').getValue(context); // 3
The core types are registered already: int, double, String, bool, List, Map, DateTime, Duration, Math, and the Java names Integer, Long, Double, Boolean, Objects, System and others.
In @Value
Every application context installs a StandardBeanExpressionResolver. Its root object is a BeanExpressionContext, so #{mailer} is the bean named mailer. To add variables or functions to every @Value expression, subclass it and override customizeEvaluationContext.
In Spring (Java)
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(root);
context.setVariable("count", 3);
parser.parseExpression("Hello #{name}", ParserContext.TEMPLATE_EXPRESSION)
.getValue(context, String.class);
@Value("#{shopProperties.name.toUpperCase()}") String banner;
The syntax and the API are the same. Differences that matter:
- No reflection. Properties and methods of your classes need
@Introspected()or an accessor. Types inT(...)andnewmust be registered as aSpelType; Java loads any class by name. getValue(context, String.class)becomesgetValueAs<String>(context).intandlongliterals are both Dartints;fanddliterals bothdoubles.matchesand the string methods are Dart's.split,replaceAllandcontainstake plain strings, not regular expressions.- No compiler mode (
SpelCompilerMode). The generator compiles the SpEL of@Queryto Dart at build time instead, so a typo there is a build error. systemPropertiesis the map of--key=valuecommand-line arguments, as Dart has no system properties.@EventListener(condition:)is a Dart function, not a SpEL string.