Skip to content

Commit d625b3d

Browse files
committed
Document SpEL IndexAccessor support in the reference manual
Closes gh-32735
1 parent 531da01 commit d625b3d

File tree

4 files changed

+136
-1
lines changed

4 files changed

+136
-1
lines changed

Diff for: framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ following kinds of expressions cannot be compiled.
516516

517517
* Expressions involving assignment
518518
* Expressions relying on the conversion service
519-
* Expressions using custom resolvers or accessors
519+
* Expressions using custom resolvers
520520
* Expressions using overloaded operators
521521
* Expressions using array construction syntax
522522
* Expressions using selection or projection

Diff for: framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ indexing into the following types of structures.
9191
* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-strings[strings]
9292
* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-maps[maps]
9393
* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-objects[objects]
94+
* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-custom[custom]
9495

9596
The following example shows how to use the safe navigation operator for indexing into
9697
a list (`?.[]`).

Diff for: framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc

+112
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,115 @@ Kotlin::
250250
----
251251
======
252252

253+
[[expressions-indexing-custom]]
254+
== Indexing into Custom Structures
255+
256+
Since Spring Framework 6.2, the Spring Expression Language supports indexing into custom
257+
structures by allowing developers to implement and register an `IndexAccessor` with the
258+
`EvaluationContext`. If you would like to support
259+
xref:core/expressions/evaluation.adoc#expressions-spel-compilation[compilation] of
260+
expressions that rely on a custom index accessor, that index accessor must implement the
261+
`CompilableIndexAccessor` SPI.
262+
263+
To support common use cases, Spring provides a built-in `ReflectiveIndexAccessor` which
264+
is a flexible `IndexAccessor` that uses reflection to read from and optionally write to
265+
an indexed structure of a target object. The indexed structure can be accessed through a
266+
`public` read-method (when being read) or a `public` write-method (when being written).
267+
The relationship between the read-method and write-method is based on a convention that
268+
is applicable for typical implementations of indexed structures.
269+
270+
NOTE: `ReflectiveIndexAccessor` also implements `CompilableIndexAccessor` in order to
271+
support xref:core/expressions/evaluation.adoc#expressions-spel-compilation[compilation]
272+
to bytecode for read access. Note, however, that the configured read-method must be
273+
invokable via a `public` class or `public` interface for compilation to succeed.
274+
275+
The following code listings define a `Color` enum and `FruitMap` type that behaves like a
276+
map but does not implement the `java.util.Map` interface. Thus, if you want to index into
277+
a `FruitMap` within a SpEL expression, you will need to register an `IndexAccessor`.
278+
279+
[source,java,indent=0,subs="verbatim,quotes"]
280+
----
281+
package example;
282+
283+
public enum Color {
284+
RED, ORANGE, YELLOW
285+
}
286+
----
287+
288+
[source,java,indent=0,subs="verbatim,quotes"]
289+
----
290+
public class FruitMap {
291+
292+
private final Map<Color, String> map = new HashMap<>();
293+
294+
public FruitMap() {
295+
this.map.put(Color.RED, "cherry");
296+
this.map.put(Color.ORANGE, "orange");
297+
this.map.put(Color.YELLOW, "banana");
298+
}
299+
300+
public String getFruit(Color color) {
301+
return this.map.get(color);
302+
}
303+
304+
public void setFruit(Color color, String fruit) {
305+
this.map.put(color, fruit);
306+
}
307+
}
308+
----
309+
310+
A read-only `IndexAccessor` for `FruitMap` can be created via `new
311+
ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit")`. With that accessor
312+
registered and a `FruitMap` registered as a variable named `#fruitMap`, the SpEL
313+
expression `#fruitMap[T(example.Color).RED]` will evaluate to `"cherry"`.
314+
315+
A read-write `IndexAccessor` for `FruitMap` can be created via `new
316+
ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit", "setFruit")`. With that
317+
accessor registered and a `FruitMap` registered as a variable named `#fruitMap`, the SpEL
318+
expression `#fruitMap[T(example.Color).RED] = 'strawberry'` can be used to change the
319+
fruit mapping for the color red from `"cherry"` to `"strawberry"`.
320+
321+
The following example demonstrates how to register a `ReflectiveIndexAccessor` to index
322+
into a `FruitMap` and then index into the `FruitMap` within a SpEL expression.
323+
324+
[tabs]
325+
======
326+
Java::
327+
+
328+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
329+
----
330+
// Create a ReflectiveIndexAccessor for FruitMap
331+
IndexAccessor fruitMapAccessor = new ReflectiveIndexAccessor(
332+
FruitMap.class, Color.class, "getFruit", "setFruit");
333+
334+
// Register the IndexAccessor for FruitMap
335+
context.addIndexAccessor(fruitMapAccessor);
336+
337+
// Register the fruitMap variable
338+
context.setVariable("fruitMap", new FruitMap());
339+
340+
// evaluates to "cherry"
341+
String fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]")
342+
.getValue(context, String.class);
343+
----
344+
345+
Kotlin::
346+
+
347+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
348+
----
349+
// Create a ReflectiveIndexAccessor for FruitMap
350+
val fruitMapAccessor = ReflectiveIndexAccessor(
351+
FruitMap::class.java, Color::class.java, "getFruit", "setFruit")
352+
353+
// Register the IndexAccessor for FruitMap
354+
context.addIndexAccessor(fruitMapAccessor)
355+
356+
// Register the fruitMap variable
357+
context.setVariable("fruitMap", FruitMap())
358+
359+
// evaluates to "cherry"
360+
val fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]")
361+
.getValue(context, String::class.java)
362+
----
363+
======
364+

Diff for: spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java

+22
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,20 @@
2626
import java.util.List;
2727
import java.util.Map;
2828

29+
import example.Color;
30+
import example.FruitMap;
2931
import org.junit.jupiter.api.Nested;
3032
import org.junit.jupiter.api.Test;
3133

3234
import org.springframework.expression.EvaluationContext;
3335
import org.springframework.expression.Expression;
3436
import org.springframework.expression.ExpressionParser;
37+
import org.springframework.expression.IndexAccessor;
3538
import org.springframework.expression.Operation;
3639
import org.springframework.expression.OperatorOverloader;
3740
import org.springframework.expression.common.TemplateParserContext;
3841
import org.springframework.expression.spel.standard.SpelExpressionParser;
42+
import org.springframework.expression.spel.support.ReflectiveIndexAccessor;
3943
import org.springframework.expression.spel.support.SimpleEvaluationContext;
4044
import org.springframework.expression.spel.support.StandardEvaluationContext;
4145
import org.springframework.expression.spel.testresources.Inventor;
@@ -254,6 +258,24 @@ void indexingIntoObjects() {
254258
assertThat(name).isEqualTo("Nikola Tesla");
255259
}
256260

261+
@Test
262+
void indexingIntoCustomStructure() {
263+
// Create a ReflectiveIndexAccessor for FruitMap
264+
IndexAccessor fruitMapAccessor = new ReflectiveIndexAccessor(
265+
FruitMap.class, Color.class, "getFruit", "setFruit");
266+
267+
// Register the IndexAccessor for FruitMap
268+
context.addIndexAccessor(fruitMapAccessor);
269+
270+
// Register the fruitMap variable
271+
context.setVariable("fruitMap", new FruitMap());
272+
273+
// evaluates to "cherry"
274+
String fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]")
275+
.getValue(context, String.class);
276+
assertThat(fruit).isEqualTo("cherry");
277+
}
278+
257279
}
258280

259281
@Nested

0 commit comments

Comments
 (0)