forked from spring-projects/spring-framework
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce ReflectiveIndexAccessor in SpEL
Closes spring-projectsgh-32714
- Loading branch information
Showing
3 changed files
with
423 additions
and
144 deletions.
There are no files selected for viewing
318 changes: 318 additions & 0 deletions
318
...on/src/main/java/org/springframework/expression/spel/support/ReflectiveIndexAccessor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,318 @@ | ||
/* | ||
* Copyright 2002-2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.springframework.expression.spel.support; | ||
|
||
import java.lang.reflect.Method; | ||
import java.lang.reflect.Modifier; | ||
|
||
import org.springframework.asm.MethodVisitor; | ||
import org.springframework.expression.EvaluationContext; | ||
import org.springframework.expression.IndexAccessor; | ||
import org.springframework.expression.TypedValue; | ||
import org.springframework.expression.spel.CodeFlow; | ||
import org.springframework.expression.spel.CompilableIndexAccessor; | ||
import org.springframework.expression.spel.SpelNode; | ||
import org.springframework.lang.Nullable; | ||
import org.springframework.util.Assert; | ||
import org.springframework.util.ClassUtils; | ||
import org.springframework.util.ReflectionUtils; | ||
|
||
/** | ||
* A flexible {@link org.springframework.expression.IndexAccessor IndexAccessor} | ||
* that uses reflection to read from and optionally write to an indexed structure | ||
* of a target instance. | ||
* | ||
* <p>An index can be accessed through a read-method (when being read) or a | ||
* write-method (when being written). The relationship between the read-method | ||
* and write-method is based on a convention that applies to typical implementations | ||
* for indexed structures. See the example below for details. | ||
* | ||
* <p>{@code ReflectiveIndexAccessor} also implements {@link CompilableIndexAccessor} | ||
* in order to support compilation to bytecode for read access. Compilation | ||
* support is enabled by default, but it can be disabled via the | ||
* {@link #ReflectiveIndexAccessor(Class, Class, String, String, boolean)} | ||
* constructor. For example, if the configured read-method is not {@code public} | ||
* and cannot be invoked via a {@code public} declaring type, compilation support | ||
* should be disabled. | ||
* | ||
* <h3>Example</h3> | ||
* | ||
* <p>The {@code FruitMap} class (the {@code targetType}) represents an indexed | ||
* structure that is indexed via the {@code Color} enum (the {@code indexType}). | ||
* The name of the read-method is {@code "getFruit"}, and that method returns | ||
* a {@code String} (the {@code indexedValueType}). The name of the write-method | ||
* is {@code "setFruit"}, and that method accepts a {@code Color} enum (the | ||
* {@code indexType}) and a {@code String} (the {@code indexedValueType} which | ||
* must match the return type of the read-method). | ||
* | ||
* <p>We can create a read-only {@code IndexAccessor} for {@code FruitMap} via | ||
* {@code new ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit")}. | ||
* With that accessor registered and a {@code FruitMap} registered as a variable | ||
* named {@code #fruitMap}, the SpEL expression {@code #fruitMap[T(example.Color).RED]} | ||
* will evaluate to {@code "cherry"}. | ||
* | ||
* <p>Similarly, we can create a read-write {@code IndexAccessor} for | ||
* {@code FruitMap} via | ||
* {@code new ReflectiveIndexAccessor(FruitMap.class, Color.class, "getFruit", "setFruit")}. | ||
* With that accessor registered and a {@code FruitMap} registered as a variable | ||
* named {@code #fruitMap}, the SpEL expression | ||
* {@code #fruitMap[T(example.Color).RED] = 'strawberry'} can be used to change | ||
* the fruit mapping for the color red from {@code "cherry"} to {@code "strawberry"}. | ||
* | ||
* <pre class="code"> | ||
* package example; | ||
* | ||
* public enum Color { | ||
* RED, ORANGE, YELLOW | ||
* }</pre> | ||
* | ||
* <pre class="code"> | ||
* 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); | ||
* } | ||
* }</pre> | ||
* | ||
* @author Sam Brannen | ||
* @since 6.2 | ||
* @see IndexAccessor | ||
* @see CompilableIndexAccessor | ||
* @see StandardEvaluationContext | ||
* @see SimpleEvaluationContext | ||
*/ | ||
public class ReflectiveIndexAccessor implements CompilableIndexAccessor { | ||
|
||
private final Class<?> targetType; | ||
|
||
private final Class<?> indexType; | ||
|
||
private final Method readMethod; | ||
|
||
private final Method readMethodToInvoke; | ||
|
||
@Nullable | ||
private final Method writeMethodToInvoke; | ||
|
||
private final boolean compilable; | ||
|
||
@Nullable | ||
private final Class<?> publicDeclaringClass; | ||
|
||
|
||
/** | ||
* Construct a new {@code ReflectiveIndexAccessor} for read-only access with | ||
* compilation support enabled. | ||
* <p>See {@linkplain ReflectiveIndexAccessor class-level documentation} for | ||
* further details and an example. | ||
* @param targetType the type of indexed structure which serves as the target | ||
* of index operations | ||
* @param indexType the type of index used to read from the indexed structure | ||
* @param readMethodName the name of the method used to read from the indexed | ||
* structure | ||
* @see #ReflectiveIndexAccessor(Class, Class, String, String) | ||
*/ | ||
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName) { | ||
this(targetType, indexType, readMethodName, null); | ||
} | ||
|
||
/** | ||
* Construct a new {@code ReflectiveIndexAccessor} for read-write access with | ||
* compilation support enabled. | ||
* <p>See {@linkplain ReflectiveIndexAccessor class-level documentation} for | ||
* further details and an example. | ||
* @param targetType the type of indexed structure which serves as the target | ||
* of index operations | ||
* @param indexType the type of index used to read from or write to the indexed | ||
* structure | ||
* @param readMethodName the name of the method used to read from the indexed | ||
* structure | ||
* @param writeMethodName the name of the method used to write to the indexed | ||
* structure, or {@code null} if writing is not supported | ||
* @see #ReflectiveIndexAccessor(Class, Class, String, String, boolean) | ||
*/ | ||
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName, | ||
@Nullable String writeMethodName) { | ||
|
||
this(targetType, indexType, readMethodName, writeMethodName, true); | ||
} | ||
|
||
/** | ||
* Construct a new {@code ReflectiveIndexAccessor} for read-write access with | ||
* configurable support for compilation. | ||
* <p>See {@linkplain ReflectiveIndexAccessor class-level documentation} for | ||
* further details and an example. | ||
* @param targetType the type of indexed structure which serves as the target | ||
* of index operations | ||
* @param indexType the type of index used to read from or write to the indexed | ||
* structure | ||
* @param readMethodName the name of the method used to read from the indexed | ||
* structure | ||
* @param writeMethodName the name of the method used to write to the indexed | ||
* structure, or {@code null} if writing is not supported | ||
* @param compilable {@code true} to support compilation to bytecode, | ||
* {@code false} to disable compilation support | ||
*/ | ||
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName, | ||
@Nullable String writeMethodName, boolean compilable) { | ||
|
||
this.targetType = targetType; | ||
this.indexType = indexType; | ||
this.compilable = compilable; | ||
|
||
Method readMethod = ReflectionUtils.findMethod(targetType, readMethodName, indexType); | ||
Assert.notNull(readMethod, () -> "Failed to find read-method '%s(%s)' in class '%s'." | ||
.formatted(readMethodName, indexType.getTypeName(), targetType.getTypeName())); | ||
this.readMethod = readMethod; | ||
this.readMethodToInvoke = ClassUtils.getInterfaceMethodIfPossible(this.readMethod, targetType); | ||
ReflectionUtils.makeAccessible(this.readMethodToInvoke); | ||
|
||
if (this.compilable) { | ||
Assert.state(Modifier.isPublic(this.readMethod.getModifiers()), | ||
() -> "Read-method must be public to support compilation: " + this.readMethod); | ||
Class<?> publicDeclaringClass = this.readMethodToInvoke.getDeclaringClass(); | ||
if (!Modifier.isPublic(publicDeclaringClass.getModifiers())) { | ||
publicDeclaringClass = CodeFlow.findPublicDeclaringClass(this.readMethod); | ||
} | ||
Assert.state(publicDeclaringClass != null && Modifier.isPublic(publicDeclaringClass.getModifiers()), | ||
() -> "Failed to find public declaring class for read-method: " + this.readMethod); | ||
this.publicDeclaringClass = publicDeclaringClass; | ||
} | ||
else { | ||
this.publicDeclaringClass = null; | ||
} | ||
|
||
if (writeMethodName != null) { | ||
Class<?> indexedValueType = this.readMethod.getReturnType(); | ||
Method writeMethod = ReflectionUtils.findMethod(targetType, writeMethodName, indexType, | ||
indexedValueType); | ||
Assert.notNull(writeMethod, () -> "Failed to find write-method '%s(%s, %s)' in class '%s'." | ||
.formatted(writeMethodName, indexType.getTypeName(), indexedValueType.getTypeName(), | ||
targetType.getTypeName())); | ||
this.writeMethodToInvoke = ClassUtils.getInterfaceMethodIfPossible(writeMethod, targetType); | ||
ReflectionUtils.makeAccessible(this.writeMethodToInvoke); | ||
} | ||
else { | ||
this.writeMethodToInvoke = null; | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Get the {@code targetType} configured via the constructor. | ||
*/ | ||
@Override | ||
public Class<?>[] getSpecificTargetClasses() { | ||
return new Class<?>[] { this.targetType }; | ||
} | ||
|
||
/** | ||
* Return {@code true} if the supplied {@code target} and {@code index} can | ||
* be assigned to the {@code targetType} and {@code indexType} configured | ||
* via the constructor. | ||
* <p>Considers primitive wrapper classes as assignable to the corresponding | ||
* primitive types. | ||
*/ | ||
@Override | ||
public boolean canRead(EvaluationContext context, Object target, Object index) { | ||
return (ClassUtils.isAssignableValue(this.targetType, target) && | ||
ClassUtils.isAssignableValue(this.indexType, index)); | ||
} | ||
|
||
/** | ||
* Invoke the configured read-method via reflection and return the result | ||
* wrapped in a {@link TypedValue}. | ||
*/ | ||
@Override | ||
public TypedValue read(EvaluationContext context, Object target, Object index) { | ||
Object value = ReflectionUtils.invokeMethod(this.readMethodToInvoke, target, index); | ||
return new TypedValue(value); | ||
} | ||
|
||
/** | ||
* Return {@code true} if a write-method has been configured and | ||
* {@link #canRead} returns {@code true} for the same arguments. | ||
*/ | ||
@Override | ||
public boolean canWrite(EvaluationContext context, Object target, Object index) { | ||
return (this.writeMethodToInvoke != null && canRead(context, target, index)); | ||
} | ||
|
||
/** | ||
* Invoke the configured write-method via reflection. | ||
* <p>Should only be invoked if {@link #canWrite} returns {@code true} for the | ||
* same arguments. | ||
*/ | ||
@Override | ||
public void write(EvaluationContext context, Object target, Object index, @Nullable Object newValue) { | ||
Assert.state(this.writeMethodToInvoke != null, "Write-method cannot be null"); | ||
ReflectionUtils.invokeMethod(this.writeMethodToInvoke, target, index, newValue); | ||
} | ||
|
||
/** | ||
* Return {@code true} if this {@code ReflectiveIndexAccessor} supports | ||
* compilation to bytecode for read access. | ||
*/ | ||
@Override | ||
public boolean isCompilable() { | ||
return this.compilable; | ||
} | ||
|
||
/** | ||
* Get the return type of the configured read-method. | ||
*/ | ||
@Override | ||
public Class<?> getIndexedValueType() { | ||
return this.readMethod.getReturnType(); | ||
} | ||
|
||
@Override | ||
public void generateCode(SpelNode index, MethodVisitor mv, CodeFlow cf) { | ||
// Ensure the current object on the stack is the target type. | ||
String lastDesc = cf.lastDescriptor(); | ||
String targetTypeDesc = CodeFlow.toDescriptor(this.targetType); | ||
if (lastDesc == null || !lastDesc.equals(targetTypeDesc)) { | ||
CodeFlow.insertCheckCast(mv, targetTypeDesc); | ||
} | ||
|
||
// Push the index onto the stack. | ||
cf.generateCodeForArgument(mv, index, this.indexType); | ||
|
||
// Invoke the read-method. | ||
Assert.state(this.publicDeclaringClass != null, "publicDeclaringClass cannot be null"); | ||
String classDesc = this.publicDeclaringClass.getName().replace('.', '/'); | ||
String methodName = this.readMethod.getName(); | ||
String methodDescr = CodeFlow.createSignatureDescriptor(this.readMethod); | ||
boolean isStatic = Modifier.isStatic(this.readMethod.getModifiers()); | ||
boolean isInterface = this.publicDeclaringClass.isInterface(); | ||
int opcode = (isStatic ? INVOKESTATIC : isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL); | ||
mv.visitMethodInsn(opcode, classDesc, methodName, methodDescr, isInterface); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
* Copyright 2002-2024 the original author or authors. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package example; | ||
|
||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
/** | ||
* Type that can be indexed by the {@link Color} enum (i.e., something other | ||
* than an int, Integer, or String) and whose indexed values are Strings. | ||
*/ | ||
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"); | ||
this.map.put(Color.GREEN, "kiwi"); | ||
this.map.put(Color.BLUE, "blueberry"); | ||
// We don't map PURPLE so that we can test for an unsupported color. | ||
} | ||
|
||
public String getFruit(Color color) { | ||
if (!this.map.containsKey(color)) { | ||
throw new IllegalArgumentException("No fruit for color " + color); | ||
} | ||
return this.map.get(color); | ||
} | ||
|
||
public void setFruit(Color color, String fruit) { | ||
this.map.put(color, fruit); | ||
} | ||
|
||
} |
Oops, something went wrong.