diff --git a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc index 9c9f6953f6ca..85b28932d13a 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/evaluation.adoc @@ -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 diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc index d8367f70df8d..3de85d474d4b 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc @@ -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 (`?.[]`). diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc index 32e6fd54e0e8..f599037490c7 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/properties-arrays.adoc @@ -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 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) +---- +====== + diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java index b512cf545181..80129b39129a 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -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; @@ -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