Skip to content

Commit 35c183d

Browse files
committed
Introduce ReflectiveIndexAccessor in SpEL
This commit introduces ReflectiveIndexAccessor for the Spring Expression Language (SpEL) which is somewhat analogous to the ReflectivePropertyAccessor implementation of PropertyAccessor. ReflectiveIndexAccessor is a flexible IndexAccessor implementation that uses reflection to read from and optionally write to an indexed structure of a target object. ReflectiveIndexAccessor also implements CompilableIndexAccessor in order to support compilation to bytecode for read access. For example, the following creates a read-write IndexAccessor for a FruitMap type that is indexed by Color, including built-in support for compilation to bytecode for read access. IndexAccessor indexAccessor = new ReflectiveIndexAccessor( FruitMap.class, Color.class, "getFruit", "setFruit"); Closes gh-32714
1 parent b232aef commit 35c183d

File tree

5 files changed

+541
-153
lines changed

5 files changed

+541
-153
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.expression.spel.support;
18+
19+
import java.lang.reflect.Method;
20+
import java.lang.reflect.Modifier;
21+
22+
import org.springframework.asm.MethodVisitor;
23+
import org.springframework.expression.EvaluationContext;
24+
import org.springframework.expression.IndexAccessor;
25+
import org.springframework.expression.TypedValue;
26+
import org.springframework.expression.spel.CodeFlow;
27+
import org.springframework.expression.spel.CompilableIndexAccessor;
28+
import org.springframework.expression.spel.SpelNode;
29+
import org.springframework.lang.Nullable;
30+
import org.springframework.util.Assert;
31+
import org.springframework.util.ClassUtils;
32+
import org.springframework.util.ReflectionUtils;
33+
34+
/**
35+
* A flexible {@link org.springframework.expression.IndexAccessor IndexAccessor}
36+
* that uses reflection to read from and optionally write to an indexed structure
37+
* of a target object.
38+
*
39+
* <p>The indexed structure can be accessed through a public read-method (when
40+
* being read) or a public write-method (when being written). The relationship
41+
* between the read-method and write-method is based on a convention that is
42+
* applicable for typical implementations of indexed structures. See the example
43+
* below for details.
44+
*
45+
* <p>{@code ReflectiveIndexAccessor} also implements {@link CompilableIndexAccessor}
46+
* in order to support compilation to bytecode for read access. Note, however,
47+
* that the configured read-method must be invokable via a public class or public
48+
* interface for compilation to succeed.
49+
*
50+
* <h3>Example</h3>
51+
*
52+
* <p>The {@code FruitMap} class (the {@code targetType}) represents a structure
53+
* that is indexed via the {@code Color} enum (the {@code indexType}). The name
54+
* of the read-method is {@code "getFruit"}, and that method returns a
55+
* {@code String} (the {@code indexedValueType}). The name of the write-method
56+
* is {@code "setFruit"}, and that method accepts a {@code Color} enum (the
57+
* {@code indexType}) and a {@code String} (the {@code indexedValueType} which
58+
* must match the return type of the read-method).
59+
*
60+
* <p>A read-only {@code IndexAccessor} for {@code FruitMap} can be created via
61+
* {@code new ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit")}.
62+
* With that accessor registered and a {@code FruitMap} registered as a variable
63+
* named {@code #fruitMap}, the SpEL expression {@code #fruitMap[T(example.Color).RED]}
64+
* will evaluate to {@code "cherry"}.
65+
*
66+
* <p>A read-write {@code IndexAccessor} for {@code FruitMap} can be created via
67+
* {@code new ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit", "setFruit")}.
68+
* With that accessor registered and a {@code FruitMap} registered as a variable
69+
* named {@code #fruitMap}, the SpEL expression
70+
* {@code #fruitMap[T(example.Color).RED] = 'strawberry'} can be used to change
71+
* the fruit mapping for the color red from {@code "cherry"} to {@code "strawberry"}.
72+
*
73+
* <pre class="code">
74+
* package example;
75+
*
76+
* public enum Color {
77+
* RED, ORANGE, YELLOW
78+
* }</pre>
79+
*
80+
* <pre class="code">
81+
* public class FruitMap {
82+
*
83+
* private final Map&lt;Color, String&gt; map = new HashMap&lt;&gt;();
84+
*
85+
* public FruitMap() {
86+
* this.map.put(Color.RED, "cherry");
87+
* this.map.put(Color.ORANGE, "orange");
88+
* this.map.put(Color.YELLOW, "banana");
89+
* }
90+
*
91+
* public String getFruit(Color color) {
92+
* return this.map.get(color);
93+
* }
94+
*
95+
* public void setFruit(Color color, String fruit) {
96+
* this.map.put(color, fruit);
97+
* }
98+
* }</pre>
99+
*
100+
* @author Sam Brannen
101+
* @since 6.2
102+
* @see IndexAccessor
103+
* @see CompilableIndexAccessor
104+
* @see StandardEvaluationContext
105+
* @see SimpleEvaluationContext
106+
*/
107+
public class ReflectiveIndexAccessor implements CompilableIndexAccessor {
108+
109+
private final Class<?> targetType;
110+
111+
private final Class<?> indexType;
112+
113+
private final Method readMethod;
114+
115+
private final Method readMethodToInvoke;
116+
117+
@Nullable
118+
private final Method writeMethodToInvoke;
119+
120+
121+
/**
122+
* Construct a new {@code ReflectiveIndexAccessor} for read-only access.
123+
* <p>See {@linkplain ReflectiveIndexAccessor class-level documentation} for
124+
* further details and an example.
125+
* @param targetType the type of indexed structure which serves as the target
126+
* of index operations
127+
* @param indexType the type of index used to read from the indexed structure
128+
* @param readMethodName the name of the method used to read from the indexed
129+
* structure
130+
*/
131+
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName) {
132+
this(targetType, indexType, readMethodName, null);
133+
}
134+
135+
/**
136+
* Construct a new {@code ReflectiveIndexAccessor} for read-write access.
137+
* <p>See {@linkplain ReflectiveIndexAccessor class-level documentation} for
138+
* further details and an example.
139+
* @param targetType the type of indexed structure which serves as the target
140+
* of index operations
141+
* @param indexType the type of index used to read from or write to the indexed
142+
* structure
143+
* @param readMethodName the name of the method used to read from the indexed
144+
* structure
145+
* @param writeMethodName the name of the method used to write to the indexed
146+
* structure, or {@code null} if writing is not supported
147+
*/
148+
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName,
149+
@Nullable String writeMethodName) {
150+
151+
this.targetType = targetType;
152+
this.indexType = indexType;
153+
154+
try {
155+
this.readMethod = targetType.getMethod(readMethodName, indexType);
156+
}
157+
catch (Exception ex) {
158+
throw new IllegalArgumentException("Failed to find public read-method '%s(%s)' in class '%s'."
159+
.formatted(readMethodName, getName(indexType), getName(targetType)));
160+
}
161+
162+
this.readMethodToInvoke = ClassUtils.getInterfaceMethodIfPossible(this.readMethod, targetType);
163+
ReflectionUtils.makeAccessible(this.readMethodToInvoke);
164+
165+
if (writeMethodName != null) {
166+
Class<?> indexedValueType = this.readMethod.getReturnType();
167+
Method writeMethod;
168+
try {
169+
writeMethod = targetType.getMethod(writeMethodName, indexType, indexedValueType);
170+
}
171+
catch (Exception ex) {
172+
throw new IllegalArgumentException("Failed to find public write-method '%s(%s, %s)' in class '%s'."
173+
.formatted(writeMethodName, getName(indexType), getName(indexedValueType),
174+
getName(targetType)));
175+
}
176+
this.writeMethodToInvoke = ClassUtils.getInterfaceMethodIfPossible(writeMethod, targetType);
177+
ReflectionUtils.makeAccessible(this.writeMethodToInvoke);
178+
}
179+
else {
180+
this.writeMethodToInvoke = null;
181+
}
182+
}
183+
184+
185+
/**
186+
* Return an array containing the {@code targetType} configured via the constructor.
187+
*/
188+
@Override
189+
public Class<?>[] getSpecificTargetClasses() {
190+
return new Class<?>[] { this.targetType };
191+
}
192+
193+
/**
194+
* Return {@code true} if the supplied {@code target} and {@code index} can
195+
* be assigned to the {@code targetType} and {@code indexType} configured
196+
* via the constructor.
197+
* <p>Considers primitive wrapper classes as assignable to the corresponding
198+
* primitive types.
199+
*/
200+
@Override
201+
public boolean canRead(EvaluationContext context, Object target, Object index) {
202+
return (ClassUtils.isAssignableValue(this.targetType, target) &&
203+
ClassUtils.isAssignableValue(this.indexType, index));
204+
}
205+
206+
/**
207+
* Invoke the configured read-method via reflection and return the result
208+
* wrapped in a {@link TypedValue}.
209+
*/
210+
@Override
211+
public TypedValue read(EvaluationContext context, Object target, Object index) {
212+
Object value = ReflectionUtils.invokeMethod(this.readMethodToInvoke, target, index);
213+
return new TypedValue(value);
214+
}
215+
216+
/**
217+
* Return {@code true} if a write-method has been configured and
218+
* {@link #canRead} returns {@code true} for the same arguments.
219+
*/
220+
@Override
221+
public boolean canWrite(EvaluationContext context, Object target, Object index) {
222+
return (this.writeMethodToInvoke != null && canRead(context, target, index));
223+
}
224+
225+
/**
226+
* Invoke the configured write-method via reflection.
227+
* <p>Should only be invoked if {@link #canWrite} returns {@code true} for the
228+
* same arguments.
229+
*/
230+
@Override
231+
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) {
232+
Assert.state(this.writeMethodToInvoke != null, "Write-method cannot be null");
233+
ReflectionUtils.invokeMethod(this.writeMethodToInvoke, target, index, newValue);
234+
}
235+
236+
@Override
237+
public boolean isCompilable() {
238+
return true;
239+
}
240+
241+
/**
242+
* Get the return type of the configured read-method.
243+
*/
244+
@Override
245+
public Class<?> getIndexedValueType() {
246+
return this.readMethod.getReturnType();
247+
}
248+
249+
@Override
250+
public void generateCode(SpelNode index, MethodVisitor mv, CodeFlow cf) {
251+
// Find the public declaring class.
252+
Class<?> publicDeclaringClass = this.readMethodToInvoke.getDeclaringClass();
253+
if (!Modifier.isPublic(publicDeclaringClass.getModifiers())) {
254+
publicDeclaringClass = CodeFlow.findPublicDeclaringClass(this.readMethod);
255+
}
256+
Assert.state(publicDeclaringClass != null && Modifier.isPublic(publicDeclaringClass.getModifiers()),
257+
() -> "Failed to find public declaring class for read-method: " + this.readMethod);
258+
String classDesc = publicDeclaringClass.getName().replace('.', '/');
259+
260+
// Ensure the current object on the stack is the required type.
261+
String lastDesc = cf.lastDescriptor();
262+
if (lastDesc == null || !classDesc.equals(lastDesc.substring(1))) {
263+
mv.visitTypeInsn(CHECKCAST, classDesc);
264+
}
265+
266+
// Push the index onto the stack.
267+
cf.generateCodeForArgument(mv, index, this.indexType);
268+
269+
// Invoke the read-method.
270+
String methodName = this.readMethod.getName();
271+
String methodDescr = CodeFlow.createSignatureDescriptor(this.readMethod);
272+
boolean isInterface = publicDeclaringClass.isInterface();
273+
int opcode = (isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL);
274+
mv.visitMethodInsn(opcode, classDesc, methodName, methodDescr, isInterface);
275+
}
276+
277+
278+
private static String getName(Class<?> clazz) {
279+
String canonicalName = clazz.getCanonicalName();
280+
return (canonicalName != null ? canonicalName : clazz.getName());
281+
}
282+
283+
}

spring-expression/src/test/java/org/springframework/expression/spel/PublicInterface.java

+2
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ public interface PublicInterface {
2323

2424
String getText();
2525

26+
String getFruit(int index);
27+
2628
}

spring-expression/src/test/java/org/springframework/expression/spel/PublicSuperclass.java

+8
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,12 @@ public String greet(String name) {
3737
return "Super, " + name;
3838
}
3939

40+
public String getIndex(int index) {
41+
return "value-" + index;
42+
}
43+
44+
public String getIndex2(int index) {
45+
return "value-" + (2 * index);
46+
}
47+
4048
}

0 commit comments

Comments
 (0)