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:

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:

See also