Skip to content

Commit 65d7762

Browse files
committed
Support SpEL compilation for public methods in private subtypes
Commit c79436f ensured that methods are invoked via a public interface or public superclass when compiling Spring Expression Language (SpEL) expressions involving method references or property access (see MethodReference, PropertyOrFieldReference, and collaborating support classes). However, compilation of expressions that access properties by indexing into an object by property name is still not properly supported in all scenarios. To address those remaining use cases, this commit ensures that methods are invoked via a public interface or public superclass when accessing a property by indexing into an object by the property name – for example, `person['name']` instead of `person.name`. In addition, SpEL's Indexer now properly relies on the CompilablePropertyAccessor abstraction instead of hard-coding support for only OptimalPropertyAccessor. This greatly reduces the complexity of the Indexer and simultaneously allows the Indexer to potentially support other CompilablePropertyAccessor implementations. Closes gh-29857
1 parent 107f47c commit 65d7762

File tree

4 files changed

+68
-40
lines changed

4 files changed

+68
-40
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/CompilablePropertyAccessor.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.springframework.asm.MethodVisitor;
2020
import org.springframework.asm.Opcodes;
2121
import org.springframework.expression.PropertyAccessor;
22+
import org.springframework.lang.Nullable;
2223

2324
/**
2425
* A compilable {@link PropertyAccessor} is able to generate bytecode that represents
@@ -41,13 +42,17 @@ public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes {
4142
Class<?> getPropertyType();
4243

4344
/**
44-
* Generate the bytecode the performs the access operation into the specified
45+
* Generate the bytecode that performs the access operation into the specified
4546
* {@link MethodVisitor} using context information from the {@link CodeFlow}
4647
* where necessary.
47-
* @param propertyName the name of the property
48+
* <p>Concrete implementations of {@code CompilablePropertyAccessor} typically
49+
* have access to the property name via other means (for example, supplied as
50+
* an argument when they were instantiated). Thus, the {@code propertyName}
51+
* supplied to this method may be {@code null}.
52+
* @param propertyName the name of the property, or {@code null} if not available
4853
* @param methodVisitor the ASM method visitor into which code should be generated
4954
* @param codeFlow the current state of the expression compiler
5055
*/
51-
void generateCode(String propertyName, MethodVisitor methodVisitor, CodeFlow codeFlow);
56+
void generateCode(@Nullable String propertyName, MethodVisitor methodVisitor, CodeFlow codeFlow);
5257

5358
}

spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@
1717
package org.springframework.expression.spel.ast;
1818

1919
import java.lang.reflect.Constructor;
20-
import java.lang.reflect.Field;
21-
import java.lang.reflect.Member;
22-
import java.lang.reflect.Method;
23-
import java.lang.reflect.Modifier;
2420
import java.util.Collection;
2521
import java.util.List;
2622
import java.util.Map;
@@ -35,6 +31,7 @@
3531
import org.springframework.expression.TypeConverter;
3632
import org.springframework.expression.TypedValue;
3733
import org.springframework.expression.spel.CodeFlow;
34+
import org.springframework.expression.spel.CompilablePropertyAccessor;
3835
import org.springframework.expression.spel.ExpressionState;
3936
import org.springframework.expression.spel.SpelEvaluationException;
4037
import org.springframework.expression.spel.SpelMessage;
@@ -223,10 +220,11 @@ else if (this.indexedType == IndexedType.MAP) {
223220
return (this.children[0] instanceof PropertyOrFieldReference || this.children[0].isCompilable());
224221
}
225222
else if (this.indexedType == IndexedType.OBJECT) {
226-
// If the string name is changing, the accessor is clearly going to change (so no compilation possible)
227-
return (this.cachedReadAccessor != null &&
228-
this.cachedReadAccessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor &&
229-
getChild(0) instanceof StringLiteral);
223+
// If the string name is changing, the accessor is clearly going to change.
224+
// So compilation is only possible if the index expression is a StringLiteral.
225+
return (getChild(0) instanceof StringLiteral &&
226+
this.cachedReadAccessor instanceof CompilablePropertyAccessor compilablePropertyAccessor &&
227+
compilablePropertyAccessor.isCompilable());
230228
}
231229
return false;
232230
}
@@ -315,30 +313,9 @@ else if (this.indexedType == IndexedType.MAP) {
315313
}
316314

317315
else if (this.indexedType == IndexedType.OBJECT) {
318-
ReflectivePropertyAccessor.OptimalPropertyAccessor accessor =
319-
(ReflectivePropertyAccessor.OptimalPropertyAccessor) this.cachedReadAccessor;
320-
Assert.state(accessor != null, "No cached read accessor");
321-
Member member = accessor.member;
322-
boolean isStatic = Modifier.isStatic(member.getModifiers());
323-
String classDesc = member.getDeclaringClass().getName().replace('.', '/');
324-
325-
if (!isStatic) {
326-
if (descriptor == null) {
327-
cf.loadTarget(mv);
328-
}
329-
if (descriptor == null || !classDesc.equals(descriptor.substring(1))) {
330-
mv.visitTypeInsn(CHECKCAST, classDesc);
331-
}
332-
}
333-
334-
if (member instanceof Method method) {
335-
mv.visitMethodInsn((isStatic? INVOKESTATIC : INVOKEVIRTUAL), classDesc, member.getName(),
336-
CodeFlow.createSignatureDescriptor(method), false);
337-
}
338-
else {
339-
mv.visitFieldInsn((isStatic ? GETSTATIC : GETFIELD), classDesc, member.getName(),
340-
CodeFlow.toJvmDescriptor(((Field) member).getType()));
341-
}
316+
CompilablePropertyAccessor compilablePropertyAccessor = (CompilablePropertyAccessor) this.cachedReadAccessor;
317+
Assert.state(compilablePropertyAccessor != null, "No cached read accessor");
318+
compilablePropertyAccessor.generateCode(null, mv, cf);
342319
}
343320

344321
cf.pushDescriptor(this.exitTypeDescriptor);
@@ -600,10 +577,8 @@ public TypedValue getValue() {
600577
Indexer.this.cachedReadAccessor = accessor;
601578
Indexer.this.cachedReadName = this.name;
602579
Indexer.this.cachedReadTargetType = targetObjectRuntimeClass;
603-
if (accessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor optimalAccessor) {
604-
Member member = optimalAccessor.member;
605-
Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(member instanceof Method method ?
606-
method.getReturnType() : ((Field) member).getType());
580+
if (accessor instanceof CompilablePropertyAccessor compilablePropertyAccessor) {
581+
Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(compilablePropertyAccessor.getPropertyType());
607582
}
608583
return accessor.read(this.evaluationContext, this.targetObject, this.name);
609584
}

spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -712,7 +712,7 @@ public Class<?> getPropertyType() {
712712
}
713713

714714
@Override
715-
public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) {
715+
public void generateCode(@Nullable String propertyName, MethodVisitor mv, CodeFlow cf) {
716716
Class<?> publicDeclaringClass = this.member.getDeclaringClass();
717717
if (!Modifier.isPublic(publicDeclaringClass.getModifiers()) && this.originalMethod != null) {
718718
publicDeclaringClass = ReflectionHelper.findPublicDeclaringClass(this.originalMethod);

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,54 @@ void privateSubclassOverridesPropertyInPublicSuperclass() {
764764
assertThat(result).isEqualTo(2);
765765
}
766766

767+
@Test
768+
void indexIntoPropertyInPrivateSubclassThatOverridesPropertyInPublicInterface() {
769+
expression = parser.parseExpression("#root['text']");
770+
PrivateSubclass privateSubclass = new PrivateSubclass();
771+
772+
// Prerequisite: type must not be public for this use case.
773+
assertNotPublic(privateSubclass.getClass());
774+
775+
String result = expression.getValue(context, privateSubclass, String.class);
776+
assertThat(result).isEqualTo("enigma");
777+
778+
assertCanCompile(expression);
779+
result = expression.getValue(context, privateSubclass, String.class);
780+
assertThat(result).isEqualTo("enigma");
781+
}
782+
783+
@Test
784+
void indexIntoPropertyInPrivateSubclassThatOverridesPropertyInPrivateInterface() {
785+
expression = parser.parseExpression("#root['message']");
786+
PrivateSubclass privateSubclass = new PrivateSubclass();
787+
788+
// Prerequisite: type must not be public for this use case.
789+
assertNotPublic(privateSubclass.getClass());
790+
791+
String result = expression.getValue(context, privateSubclass, String.class);
792+
assertThat(result).isEqualTo("hello");
793+
794+
assertCanCompile(expression);
795+
result = expression.getValue(context, privateSubclass, String.class);
796+
assertThat(result).isEqualTo("hello");
797+
}
798+
799+
@Test
800+
void indexIntoPropertyInPrivateSubclassThatOverridesPropertyInPublicSuperclass() {
801+
expression = parser.parseExpression("#root['number']");
802+
PrivateSubclass privateSubclass = new PrivateSubclass();
803+
804+
// Prerequisite: type must not be public for this use case.
805+
assertNotPublic(privateSubclass.getClass());
806+
807+
Integer result = expression.getValue(context, privateSubclass, Integer.class);
808+
assertThat(result).isEqualTo(2);
809+
810+
assertCanCompile(expression);
811+
result = expression.getValue(context, privateSubclass, Integer.class);
812+
assertThat(result).isEqualTo(2);
813+
}
814+
767815
private interface PrivateInterface {
768816

769817
String getMessage();

0 commit comments

Comments
 (0)