Polish SpEL internals

This commit is contained in:
Sam Brannen
2026-04-12 14:20:50 +02:00
parent fb34264169
commit 59f9cf8645
5 changed files with 60 additions and 44 deletions
@@ -16,6 +16,7 @@
package org.springframework.expression.spel.ast;
import java.util.Objects;
import java.util.Optional;
import org.springframework.asm.Label;
@@ -25,7 +26,6 @@ import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Represents the Elvis operator {@code ?:}.
@@ -60,6 +60,7 @@ public class Elvis extends SpelNodeImpl {
*/
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
TypedValue result;
TypedValue leftHandTypedValue = this.children[0].getValueInternal(state);
Object leftHandValue = leftHandTypedValue.getValue();
@@ -67,20 +68,21 @@ public class Elvis extends SpelNodeImpl {
// Compilation is currently not supported for Optional with the Elvis operator.
this.exitTypeDescriptor = null;
if (optional.isPresent()) {
return new TypedValue(optional.get());
result = new TypedValue(optional.get());
}
else {
result = this.children[1].getValueInternal(state);
}
return this.children[1].getValueInternal(state);
}
// If this check is changed, the generateCode method will need changing too
if (leftHandValue != null && !"".equals(leftHandValue)) {
return leftHandTypedValue;
// If this check is changed, the generateCode() method will need changing too
else if (leftHandValue != null && !"".equals(leftHandValue)) {
result = leftHandTypedValue;
}
else {
TypedValue result = this.children[1].getValueInternal(state);
computeExitTypeDescriptor();
return result;
result = this.children[1].getValueInternal(state);
}
computeExitTypeDescriptor();
return result;
}
@Override
@@ -92,22 +94,25 @@ public class Elvis extends SpelNodeImpl {
public boolean isCompilable() {
SpelNodeImpl condition = this.children[0];
SpelNodeImpl ifNullValue = this.children[1];
String conditionDescriptor = condition.exitTypeDescriptor;
String ifNullValueDescriptor = ifNullValue.exitTypeDescriptor;
return (condition.isCompilable() && ifNullValue.isCompilable() &&
condition.exitTypeDescriptor != null && ifNullValue.exitTypeDescriptor != null);
conditionDescriptor != null && ifNullValueDescriptor != null);
}
@Override
public void generateCode(MethodVisitor mv, CodeFlow cf) {
// exit type descriptor can be null if both components are literal expressions
computeExitTypeDescriptor();
Label elseTarget = new Label();
Label endOfIf = new Label();
cf.enterCompilationScope();
this.children[0].generateCode(mv, cf);
String lastDesc = cf.lastDescriptor();
Assert.state(lastDesc != null, "No last descriptor");
CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0));
cf.exitCompilationScope();
Label elseTarget = new Label();
Label endOfIf = new Label();
mv.visitInsn(DUP);
mv.visitJumpInsn(IFNULL, elseTarget);
// Also check if empty string, as per the code in the interpreted version
@@ -116,6 +121,7 @@ public class Elvis extends SpelNodeImpl {
mv.visitInsn(SWAP);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z",false);
mv.visitJumpInsn(IFEQ, endOfIf); // if not empty, drop through to elseTarget
mv.visitLabel(elseTarget);
mv.visitInsn(POP);
cf.enterCompilationScope();
@@ -126,16 +132,20 @@ public class Elvis extends SpelNodeImpl {
CodeFlow.insertBoxIfNecessary(mv, lastDesc.charAt(0));
}
cf.exitCompilationScope();
mv.visitLabel(endOfIf);
cf.pushDescriptor(this.exitTypeDescriptor);
}
private void computeExitTypeDescriptor() {
if (this.exitTypeDescriptor == null && this.children[0].exitTypeDescriptor != null &&
this.children[1].exitTypeDescriptor != null) {
String conditionDescriptor = this.children[0].exitTypeDescriptor;
String ifNullValueDescriptor = this.children[1].exitTypeDescriptor;
if (ObjectUtils.nullSafeEquals(conditionDescriptor, ifNullValueDescriptor)) {
SpelNodeImpl condition = this.children[0];
SpelNodeImpl ifNullValue = this.children[1];
String conditionDescriptor = condition.exitTypeDescriptor;
String ifNullValueDescriptor = ifNullValue.exitTypeDescriptor;
if (this.exitTypeDescriptor == null && conditionDescriptor != null && ifNullValueDescriptor != null) {
if (Objects.equals(conditionDescriptor, ifNullValueDescriptor)) {
this.exitTypeDescriptor = conditionDescriptor;
}
else {
@@ -23,6 +23,7 @@ import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.StringJoiner;
@@ -45,7 +46,6 @@ import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.support.ReflectiveMethodExecutor;
import org.springframework.expression.spel.support.ReflectiveMethodResolver;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Expression language AST node that represents a method reference (i.e., a
@@ -476,7 +476,7 @@ public class MethodReference extends SpelNodeImpl {
public boolean isSuitable(Object target, @Nullable TypeDescriptor targetType, List<TypeDescriptor> argumentTypes) {
return ((this.staticClass == null || this.staticClass == target) &&
ObjectUtils.nullSafeEquals(this.targetType, targetType) && this.argumentTypes.equals(argumentTypes));
Objects.equals(this.targetType, targetType) && this.argumentTypes.equals(argumentTypes));
}
public boolean hasProxyTarget() {
@@ -18,6 +18,7 @@ package org.springframework.expression.spel.ast;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
@@ -363,11 +364,11 @@ public abstract class Operator extends SpelNodeImpl {
boolean rightNumeric = CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(rd);
// If the declared descriptors aren't providing the information, try the actual descriptors
if (!leftNumeric && !ObjectUtils.nullSafeEquals(ld, leftActualDescriptor)) {
if (!leftNumeric && !Objects.equals(ld, leftActualDescriptor)) {
ld = leftActualDescriptor;
leftNumeric = CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(ld);
}
if (!rightNumeric && !ObjectUtils.nullSafeEquals(rd, rightActualDescriptor)) {
if (!rightNumeric && !Objects.equals(rd, rightActualDescriptor)) {
rd = rightActualDescriptor;
rightNumeric = CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(rd);
}
@@ -54,5 +54,4 @@ public class RealLiteral extends Literal {
cf.pushDescriptor(this.exitTypeDescriptor);
}
}
@@ -16,6 +16,8 @@
package org.springframework.expression.spel.ast;
import java.util.Objects;
import org.springframework.asm.Label;
import org.springframework.asm.MethodVisitor;
import org.springframework.expression.EvaluationException;
@@ -25,7 +27,6 @@ import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Represents a ternary expression, for example: "someCheck()?true:false".
@@ -66,35 +67,26 @@ public class Ternary extends SpelNodeImpl {
return "(" + getChild(0).toStringAST() + " ? " + getChild(1).toStringAST() + " : " + getChild(2).toStringAST() + ")";
}
private void computeExitTypeDescriptor() {
if (this.exitTypeDescriptor == null && this.children[1].exitTypeDescriptor != null &&
this.children[2].exitTypeDescriptor != null) {
String leftDescriptor = this.children[1].exitTypeDescriptor;
String rightDescriptor = this.children[2].exitTypeDescriptor;
if (ObjectUtils.nullSafeEquals(leftDescriptor, rightDescriptor)) {
this.exitTypeDescriptor = leftDescriptor;
}
else {
// Use the easiest to compute common supertype
this.exitTypeDescriptor = "Ljava/lang/Object";
}
}
}
@Override
public boolean isCompilable() {
SpelNodeImpl condition = this.children[0];
SpelNodeImpl left = this.children[1];
SpelNodeImpl right = this.children[2];
String conditionDescriptor = condition.exitTypeDescriptor;
String leftDescriptor = left.exitTypeDescriptor;
String rightDescriptor = right.exitTypeDescriptor;
return (condition.isCompilable() && left.isCompilable() && right.isCompilable() &&
CodeFlow.isBooleanCompatible(condition.exitTypeDescriptor) &&
left.exitTypeDescriptor != null && right.exitTypeDescriptor != null);
CodeFlow.isBooleanCompatible(conditionDescriptor) &&
leftDescriptor != null && rightDescriptor != null);
}
@Override
public void generateCode(MethodVisitor mv, CodeFlow cf) {
// May reach here without it computed if all elements are literals
// May get here without the exit descriptor having been computed, if
// all elements are literals.
computeExitTypeDescriptor();
cf.enterCompilationScope();
this.children[0].generateCode(mv, cf);
String lastDesc = cf.lastDescriptor();
@@ -128,4 +120,18 @@ public class Ternary extends SpelNodeImpl {
cf.pushDescriptor(this.exitTypeDescriptor);
}
private void computeExitTypeDescriptor() {
String leftDescriptor = this.children[1].exitTypeDescriptor;
String rightDescriptor = this.children[2].exitTypeDescriptor;
if (this.exitTypeDescriptor == null && leftDescriptor != null && rightDescriptor != null) {
if (Objects.equals(leftDescriptor, rightDescriptor)) {
this.exitTypeDescriptor = leftDescriptor;
}
else {
// Use the easiest to compute common supertype
this.exitTypeDescriptor = "Ljava/lang/Object";
}
}
}
}