
# 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.

```dart
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 / 2` is `3`. `%` keeps the sign of the left operand. An integer division by zero throws.
- `+` concatenates when either side is a string: `'n=' + 1` is `n=1`.
- Numbers compare by value across `int` and `double` (`2 == 2.0`). Lists, sets and maps compare deeply.
- Elvis (`?:`) also replaces an empty string, not only `null`.
- Selection and projection work on lists, sets and maps. On a map, `#this` is a `MapEntry` with `key` and `value`.
- A template that is a single `#{...}` keeps the type of its value: `'#{2}'` gives the `int` 2. Text around it makes a `String`.

## 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.

```dart
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`:

```dart
// 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:

```dart
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 |

```dart
// 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)

```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 in `T(...)` and `new` must be registered as a `SpelType`; Java loads any class by name.
- `getValue(context, String.class)` becomes `getValueAs<String>(context)`.
- `int` and `long` literals are both Dart `int`s; `f` and `d` literals both `double`s.
- `matches` and the string methods are Dart's. `split`, `replaceAll` and `contains` take plain strings, not regular expressions.
- No compiler mode (`SpelCompilerMode`). The generator compiles the SpEL of `@Query` to Dart at build time instead, so a typo there is a build error.
- `systemProperties` is the map of `--key=value` command-line arguments, as Dart has no system properties.
- `@EventListener(condition:)` is a Dart function, not a SpEL string.

## See also

- [@Value](../config/value.md)
- [Placeholders](../config/placeholders.md)
- [Environment](../config/environment.md)
- [Type conversion](../conversion/index.md)
- [@EventListener](../events/event-listener.md)
- [JSON](../json/index.md)
