Merge branch '7.0.x'

This commit is contained in:
Sam Brannen
2026-07-26 10:59:55 +03:00
2 changed files with 82 additions and 4 deletions
@@ -756,9 +756,10 @@ public class Indexer extends SpelNodeImpl {
if (cachedPropertyReadState != null) {
String cachedPropertyName = cachedPropertyReadState.name;
Class<?> cachedTargetType = cachedPropertyReadState.targetType;
PropertyAccessor accessor = cachedPropertyReadState.accessor;
// Is it OK to use the cached accessor?
if (cachedPropertyName.equals(this.name) && cachedTargetType.equals(targetType)) {
PropertyAccessor accessor = cachedPropertyReadState.accessor;
if (cachedPropertyName.equals(this.name) && cachedTargetType.equals(targetType) &&
this.evaluationContext.getPropertyAccessors().contains(accessor)) {
this.expressionState.trackOperation();
return accessor.read(this.evaluationContext, this.targetObject, this.name);
}
@@ -800,9 +801,10 @@ public class Indexer extends SpelNodeImpl {
if (cachedPropertyWriteState != null) {
String cachedPropertyName = cachedPropertyWriteState.name;
Class<?> cachedTargetType = cachedPropertyWriteState.targetType;
PropertyAccessor accessor = cachedPropertyWriteState.accessor;
// Is it OK to use the cached accessor?
if (cachedPropertyName.equals(this.name) && cachedTargetType.equals(targetType)) {
PropertyAccessor accessor = cachedPropertyWriteState.accessor;
if (cachedPropertyName.equals(this.name) && cachedTargetType.equals(targetType) &&
this.evaluationContext.getPropertyAccessors().contains(accessor)) {
this.expressionState.trackOperation();
accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
return;
@@ -849,6 +849,82 @@ class IndexingTests {
}
}
@Nested
class PropertyAccessorValueRefTests { // gh-36986
private final StandardEvaluationContext context = new StandardEvaluationContext();
private final SpelExpressionParser parser = new SpelExpressionParser();
@Test
void readIndexDoesNotUseRemovedPropertyAccessor() {
Person person = new Person("Jane");
this.context.setVariable("person", person);
PropertyAccessor accessor = new UppercasingPropertyAccessor();
this.context.addPropertyAccessor(accessor);
Expression expression = this.parser.parseExpression("#person['name']");
// The first evaluation resolves and caches the custom accessor.
assertThat(expression.getValue(this.context)).isEqualTo("JANE");
// Simulate an application reconfiguring the context at runtime.
this.context.removePropertyAccessor(accessor);
// The removed accessor must not be reused for subsequent evaluations.
assertThat(expression.getValue(this.context)).isEqualTo("Jane");
}
@Test
void writeIndexDoesNotUseRemovedPropertyAccessor() {
Person person = new Person("Jane");
this.context.setVariable("person", person);
PropertyAccessor accessor = new UppercasingPropertyAccessor();
this.context.addPropertyAccessor(accessor);
Expression expression = this.parser.parseExpression("#person['name']");
// The first write resolves and caches the custom accessor.
expression.setValue(this.context, "Alice");
assertThat(person.getName()).isEqualTo("custom:Alice");
// Simulate an application reconfiguring the context at runtime.
this.context.removePropertyAccessor(accessor);
// The removed accessor must not be reused for subsequent writes.
expression.setValue(this.context, "Bob");
assertThat(person.getName()).isEqualTo("Bob");
}
private static class UppercasingPropertyAccessor implements PropertyAccessor {
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class<?>[] {Person.class};
}
@Override
public boolean canRead(EvaluationContext context, @Nullable Object target, String name) {
return "name".equals(name);
}
@Override
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) {
return new TypedValue(((Person) target).getName().toUpperCase());
}
@Override
public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) {
return "name".equals(name);
}
@Override
public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) {
((Person) target).setName("custom:" + newValue);
}
}
}
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)