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
332 additions
and
144 deletions.
There are no files selected for viewing
227 changes: 227 additions & 0 deletions
227
...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,227 @@ | ||
/* | ||
* 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 powerful {@link org.springframework.expression.IndexAccessor IndexAccessor} | ||
* that uses reflection to read from (and possibly 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), and the relationship between the read-method | ||
* and write-method is based on a convention that applies to typical implementations | ||
* for indexed structures. | ||
* | ||
* <p>Also supports compilation to bytecode for read access via the | ||
* {@link CompilableIndexAccessor} SPI. | ||
* | ||
* <h3>Example</h3> | ||
* | ||
* <p>The {@code FruitMap} class below (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 is inferred from the return type of the | ||
* read-method). | ||
* | ||
* <p>We can create a read-only {@code IndexAccessor} by configuring the read-method 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} by configuring | ||
* the read-method and the write-method 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 red fruit 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; | ||
|
||
|
||
/** | ||
* Construct a new {@code ReflectiveIndexAccessor} for read-only access. | ||
*/ | ||
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName) { | ||
this(targetType, indexType, readMethodName, null); | ||
} | ||
|
||
/** | ||
* Construct a new {@code ReflectiveIndexAccessor} for read-write access. | ||
*/ | ||
public ReflectiveIndexAccessor(Class<?> targetType, Class<?> indexType, String readMethodName, | ||
@Nullable String writeMethodName) { | ||
|
||
this.targetType = targetType; | ||
this.indexType = indexType; | ||
|
||
Method readMethod = ReflectionUtils.findMethod(targetType, readMethodName, indexType); | ||
Assert.notNull(readMethod, () -> "Failed to find 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 (writeMethodName != null) { | ||
Class<?> indexedValueType = this.readMethod.getReturnType(); | ||
Method writeMethod = ReflectionUtils.findMethod(targetType, writeMethodName, indexType, | ||
indexedValueType); | ||
Assert.notNull(writeMethod, () -> "Failed to find 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; | ||
} | ||
} | ||
|
||
|
||
@Override | ||
public Class<?>[] getSpecificTargetClasses() { | ||
return new Class<?>[] { this.targetType }; | ||
} | ||
|
||
@Override | ||
public boolean canRead(EvaluationContext context, Object target, Object index) { | ||
return (ClassUtils.isAssignableValue(this.targetType, target) && | ||
ClassUtils.isAssignableValue(this.indexType, index)); | ||
} | ||
|
||
@Override | ||
public TypedValue read(EvaluationContext context, Object target, Object index) { | ||
Object value = ReflectionUtils.invokeMethod(this.readMethodToInvoke, target, index); | ||
return new TypedValue(value); | ||
} | ||
|
||
@Override | ||
public boolean canWrite(EvaluationContext context, Object target, Object index) { | ||
return (this.writeMethodToInvoke != null && canRead(context, target, index)); | ||
} | ||
|
||
@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); | ||
} | ||
|
||
@Override | ||
public boolean isCompilable() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public Class<?> getIndexedValueType() { | ||
return this.readMethod.getReturnType(); | ||
} | ||
|
||
@Override | ||
public void generateCode(SpelNode index, MethodVisitor mv, CodeFlow cf) { | ||
// Determine the public declaring class. | ||
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: " + this.readMethod); | ||
|
||
// 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. | ||
String classDesc = publicDeclaringClass.getName().replace('.', '/'); | ||
String methodName = this.readMethod.getName(); | ||
String methodDescr = CodeFlow.createSignatureDescriptor(this.readMethod); | ||
boolean isStatic = Modifier.isStatic(this.readMethod.getModifiers()); | ||
boolean isInterface = 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.