From af2934c09b8fa489e4285b33bbff48899c1341e7 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Thu, 1 Feb 2024 17:24:25 +0100 Subject: [PATCH] Document support for overloading operators in SpEL in reference manual Closes gh-32182 --- .../expressions/language-ref/operators.adoc | 71 +++++++++++++++++++ .../spel/SpelDocumentationTests.java | 38 ++++++++++ 2 files changed, 109 insertions(+) diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc index c68e372790c..488022a7038 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operators.adoc @@ -8,6 +8,8 @@ The Spring Expression Language supports the following kinds of operators: * xref:core/expressions/language-ref/operators.adoc#expressions-operators-string[String Operators] * xref:core/expressions/language-ref/operators.adoc#expressions-operators-mathematical[Mathematical Operators] * xref:core/expressions/language-ref/operators.adoc#expressions-assignment[The Assignment Operator] +* xref:core/expressions/language-ref/operators.adoc#expressions-operators-overloaded[Overloaded Operators] + [[expressions-operators-relational]] @@ -523,3 +525,72 @@ Kotlin:: ====== +[[expressions-operators-overloaded]] +== Overloaded Operators + +By default, the mathematical operations defined in SpEL's `Operation` enum (`ADD`, +`SUBTRACT`, `DIVIDE`, `MULTIPLY`, `MODULUS`, and `POWER`) support simple types like +numbers. By providing an implementation of `OperatorOverloader`, the expression language +can support these operations on other types. + +For example, if we want to overload the `ADD` operator to allow two lists to be +concatenated using the `+` sign, we can implement a custom `OperatorOverloader` as +follows. + +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + pubic class ListConcatenation implements OperatorOverloader { + + @Override + public boolean overridesOperation(Operation operation, Object left, Object right) { + return (operation == Operation.ADD && + left instanceof List && right instanceof List); + } + + @Override + @SuppressWarnings("unchecked") + public Object operate(Operation operation, Object left, Object right) { + if (operation == Operation.ADD && + left instanceof List list1 && right instanceof List list2) { + + List result = new ArrayList(list1); + result.addAll(list2); + return result; + } + throw new UnsupportedOperationException( + "No overload for operation %s and operands [%s] and [%s]" + .formatted(operation.name(), left, right)); + } + } +---- + +If we register `ListConcatenation` as the `OperatorOverloader` in a +`StandardEvaluationContext`, we can then evaluate expressions like `{1, 2, 3} + {4, 5}` +as demonstrated in the following example. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setOperatorOverloader(new ListConcatenation()); + + // evaluates to a new list: [1, 2, 3, 4, 5] + parser.parseExpression("{1, 2, 3} + {4, 5}").getValue(context, List.class); +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"] +---- + StandardEvaluationContext context = StandardEvaluationContext() + context.setOperatorOverloader(ListConcatenation()) + + // evaluates to a new list: [1, 2, 3, 4, 5] + parser.parseExpression("{1, 2, 3} + {4, 5}").getValue(context, List::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 9752c3c7aa0..ddede6f1f04 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 @@ -32,6 +32,8 @@ 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.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.SimpleEvaluationContext; @@ -447,6 +449,18 @@ class SpelDocumentationTests extends AbstractExpressionTests { assertThat(parser.parseExpression("foo").getValue(context, inventor, String.class)).isEqualTo("Alexandar Seovic"); assertThat(aleks).isEqualTo("Alexandar Seovic"); } + + @Test + @SuppressWarnings("unchecked") + void overloadingOperators() { + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setOperatorOverloader(new ListConcatenation()); + + // evaluates to [1, 2, 3, 4, 5] + List list = parser.parseExpression("{1, 2, 3} + {4, 5}").getValue(context, List.class); + assertThat(list).containsExactly(1, 2, 3, 4, 5); + } + } @Nested @@ -672,4 +686,28 @@ class SpelDocumentationTests extends AbstractExpressionTests { } } + private static class ListConcatenation implements OperatorOverloader { + + @Override + public boolean overridesOperation(Operation operation, Object left, Object right) { + return (operation == Operation.ADD && + left instanceof List && right instanceof List); + } + + @Override + @SuppressWarnings("unchecked") + public Object operate(Operation operation, Object left, Object right) { + if (operation == Operation.ADD && + left instanceof List list1 && right instanceof List list2) { + + List result = new ArrayList(list1); + result.addAll(list2); + return result; + } + throw new UnsupportedOperationException( + "No overload for operation %s and operands [%s] and [%s]" + .formatted(operation.name(), left, right)); + } + } + }