Skip to content

Commit

Permalink
Document SpEL IndexAccessor support in the reference manual
Browse files Browse the repository at this point in the history
Closes gh-32735
  • Loading branch information
sbrannen committed May 15, 2024
1 parent 531da01 commit d625b3d
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ following kinds of expressions cannot be compiled.

* Expressions involving assignment
* Expressions relying on the conversion service
* Expressions using custom resolvers or accessors
* Expressions using custom resolvers
* Expressions using overloaded operators
* Expressions using array construction syntax
* Expressions using selection or projection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ indexing into the following types of structures.
* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-strings[strings]
* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-maps[maps]
* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-objects[objects]
* xref:core/expressions/language-ref/properties-arrays.adoc#expressions-indexing-custom[custom]

The following example shows how to use the safe navigation operator for indexing into
a list (`?.[]`).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,115 @@ Kotlin::
----
======

[[expressions-indexing-custom]]
== Indexing into Custom Structures

Since Spring Framework 6.2, the Spring Expression Language supports indexing into custom
structures by allowing developers to implement and register an `IndexAccessor` with the
`EvaluationContext`. If you would like to support
xref:core/expressions/evaluation.adoc#expressions-spel-compilation[compilation] of
expressions that rely on a custom index accessor, that index accessor must implement the
`CompilableIndexAccessor` SPI.

To support common use cases, Spring provides a built-in `ReflectiveIndexAccessor` which
is a flexible `IndexAccessor` that uses reflection to read from and optionally write to
an indexed structure of a target object. The indexed structure can be accessed through a
`public` read-method (when being read) or a `public` write-method (when being written).
The relationship between the read-method and write-method is based on a convention that
is applicable for typical implementations of indexed structures.

NOTE: `ReflectiveIndexAccessor` also implements `CompilableIndexAccessor` in order to
support xref:core/expressions/evaluation.adoc#expressions-spel-compilation[compilation]
to bytecode for read access. Note, however, that the configured read-method must be
invokable via a `public` class or `public` interface for compilation to succeed.

The following code listings define a `Color` enum and `FruitMap` type that behaves like a
map but does not implement the `java.util.Map` interface. Thus, if you want to index into
a `FruitMap` within a SpEL expression, you will need to register an `IndexAccessor`.

[source,java,indent=0,subs="verbatim,quotes"]
----
package example;
public enum Color {
RED, ORANGE, YELLOW
}
----

[source,java,indent=0,subs="verbatim,quotes"]
----
public class FruitMap {
private final Map<Color, String> map = new HashMap<>();
public FruitMap() {
this.map.put(Color.RED, "cherry");
this.map.put(Color.ORANGE, "orange");
this.map.put(Color.YELLOW, "banana");
}
public String getFruit(Color color) {
return this.map.get(color);
}
public void setFruit(Color color, String fruit) {
this.map.put(color, fruit);
}
}
----

A read-only `IndexAccessor` for `FruitMap` can be created via `new
ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit")`. With that accessor
registered and a `FruitMap` registered as a variable named `#fruitMap`, the SpEL
expression `#fruitMap[T(example.Color).RED]` will evaluate to `"cherry"`.

A read-write `IndexAccessor` for `FruitMap` can be created via `new
ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit", "setFruit")`. With that
accessor registered and a `FruitMap` registered as a variable named `#fruitMap`, the SpEL
expression `#fruitMap[T(example.Color).RED] = 'strawberry'` can be used to change the
fruit mapping for the color red from `"cherry"` to `"strawberry"`.

The following example demonstrates how to register a `ReflectiveIndexAccessor` to index
into a `FruitMap` and then index into the `FruitMap` within a SpEL expression.

[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
// Create a ReflectiveIndexAccessor for FruitMap
IndexAccessor fruitMapAccessor = new ReflectiveIndexAccessor(
FruitMap.class, Color.class, "getFruit", "setFruit");
// Register the IndexAccessor for FruitMap
context.addIndexAccessor(fruitMapAccessor);
// Register the fruitMap variable
context.setVariable("fruitMap", new FruitMap());
// evaluates to "cherry"
String fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]")
.getValue(context, String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
// Create a ReflectiveIndexAccessor for FruitMap
val fruitMapAccessor = ReflectiveIndexAccessor(
FruitMap::class.java, Color::class.java, "getFruit", "setFruit")
// Register the IndexAccessor for FruitMap
context.addIndexAccessor(fruitMapAccessor)
// Register the fruitMap variable
context.setVariable("fruitMap", FruitMap())
// evaluates to "cherry"
val fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]")
.getValue(context, String::class.java)
----
======

Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,20 @@
import java.util.List;
import java.util.Map;

import example.Color;
import example.FruitMap;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.IndexAccessor;
import org.springframework.expression.Operation;
import org.springframework.expression.OperatorOverloader;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.ReflectiveIndexAccessor;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.testresources.Inventor;
Expand Down Expand Up @@ -254,6 +258,24 @@ void indexingIntoObjects() {
assertThat(name).isEqualTo("Nikola Tesla");
}

@Test
void indexingIntoCustomStructure() {
// Create a ReflectiveIndexAccessor for FruitMap
IndexAccessor fruitMapAccessor = new ReflectiveIndexAccessor(
FruitMap.class, Color.class, "getFruit", "setFruit");

// Register the IndexAccessor for FruitMap
context.addIndexAccessor(fruitMapAccessor);

// Register the fruitMap variable
context.setVariable("fruitMap", new FruitMap());

// evaluates to "cherry"
String fruit = parser.parseExpression("#fruitMap[T(example.Color).RED]")
.getValue(context, String.class);
assertThat(fruit).isEqualTo("cherry");
}

}

@Nested
Expand Down

0 comments on commit d625b3d

Please sign in to comment.