Skip to content

Commit 38cd1ec

Browse files
aclementjhoeller
authored andcommitted
Rework compilation of OpNE/OpEQ SpEL operators
For SPR-14863 we need to adjust the code generation for OpNE to use !x.equals(y) rather than x!=y. There are also further cases in the equalityCheck() code in Operator that were not being handled in the compilation case (when comparators are used for example). This latter issue also affects OpEQ. Rather than add yet more bytecode generation, both OpNE and OpEQ generateCode() methods have been simplified. The generated code now delegates to equalityCheck() in Operator which is exactly what the interpreted case does. This ensures that the compiled code continues to behave just like the interpreted case. It ensures changes to the interpreted case are automatically picked up for the compiled case. It makes the bytecode generation simpler. The benefit of compilation of SpEL expressions is to avoid slow reflective calls - that doesn't apply for a basic (in)equality test so there is no need to go crazy in bytecode gen. Issue: SPR-14863 (cherry picked from commit 9000acd)
1 parent 77e00f1 commit 38cd1ec

File tree

5 files changed

+1425
-1312
lines changed

5 files changed

+1425
-1312
lines changed

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

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,28 @@
2828
import org.springframework.util.Assert;
2929

3030
/**
31-
* Manages the class being generated by the compilation process. It records
32-
* intermediate compilation state as the bytecode is generated. It also includes
33-
* various bytecode generation helper functions.
31+
* Manages the class being generated by the compilation process.
32+
*
33+
* <p>Records intermediate compilation state as the bytecode is generated.
34+
* Also includes various bytecode generation helper functions.
3435
*
3536
* @author Andy Clement
37+
* @author Juergen Hoeller
3638
* @since 4.1
3739
*/
3840
public class CodeFlow implements Opcodes {
3941

42+
/**
43+
* Name of the class being generated. Typically used when generating code
44+
* that accesses freshly generated fields on the generated type.
45+
*/
46+
private final String className;
47+
48+
/**
49+
* The current class being generated.
50+
*/
51+
private final ClassWriter classWriter;
52+
4053
/**
4154
* Record the type of what is on top of the bytecode stack (i.e. the type of the
4255
* output from the previous expression component). New scopes are used to evaluate
@@ -45,31 +58,20 @@ public class CodeFlow implements Opcodes {
4558
*/
4659
private final Stack<ArrayList<String>> compilationScopes;
4760

48-
/**
49-
* The current class being generated
50-
*/
51-
private ClassWriter cw;
52-
5361
/**
5462
* As SpEL ast nodes are called to generate code for the main evaluation method
5563
* they can register to add a field to this class. Any registered FieldAdders
5664
* will be called after the main evaluation function has finished being generated.
5765
*/
58-
private List<FieldAdder> fieldAdders = null;
66+
private List<FieldAdder> fieldAdders;
5967

6068
/**
6169
* As SpEL ast nodes are called to generate code for the main evaluation method
6270
* they can register to add code to a static initializer in the class. Any
6371
* registered ClinitAdders will be called after the main evaluation function
6472
* has finished being generated.
6573
*/
66-
private List<ClinitAdder> clinitAdders = null;
67-
68-
/**
69-
* Name of the class being generated. Typically used when generating code
70-
* that accesses freshly generated fields on the generated type.
71-
*/
72-
private String clazzName;
74+
private List<ClinitAdder> clinitAdders;
7375

7476
/**
7577
* When code generation requires holding a value in a class level field, this
@@ -83,13 +85,20 @@ public class CodeFlow implements Opcodes {
8385
*/
8486
private int nextFreeVariableId = 1;
8587

86-
public CodeFlow(String clazzName, ClassWriter cw) {
88+
89+
/**
90+
* Construct a new {@code CodeFlow} for the given class.
91+
* @param className the name of the class
92+
* @param classWriter the corresponding ASM {@code ClassWriter}
93+
*/
94+
public CodeFlow(String className, ClassWriter classWriter) {
95+
this.className = className;
96+
this.classWriter = classWriter;
8797
this.compilationScopes = new Stack<ArrayList<String>>();
8898
this.compilationScopes.add(new ArrayList<String>());
89-
this.cw = cw;
90-
this.clazzName = clazzName;
9199
}
92100

101+
93102
/**
94103
* Push the byte code to load the target (i.e. what was passed as the first argument
95104
* to CompiledExpression.getValue(target, context))
@@ -99,6 +108,16 @@ public void loadTarget(MethodVisitor mv) {
99108
mv.visitVarInsn(ALOAD, 1);
100109
}
101110

111+
/**
112+
* Push the bytecode to load the EvaluationContext (the second parameter passed to
113+
* the compiled expression method).
114+
* @param mv the visitor into which the load instruction should be inserted
115+
* @since 4.3.4
116+
*/
117+
public void loadEvaluationContext(MethodVisitor mv) {
118+
mv.visitVarInsn(ALOAD, 2);
119+
}
120+
102121
/**
103122
* Record the descriptor for the most recently evaluated expression element.
104123
* @param descriptor type descriptor for most recently evaluated element
@@ -155,18 +174,18 @@ public void unboxBooleanIfNecessary(MethodVisitor mv) {
155174
public void finish() {
156175
if (this.fieldAdders != null) {
157176
for (FieldAdder fieldAdder : this.fieldAdders) {
158-
fieldAdder.generateField(cw,this);
177+
fieldAdder.generateField(this.classWriter, this);
159178
}
160179
}
161180
if (this.clinitAdders != null) {
162-
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "<clinit>", "()V", null, null);
181+
MethodVisitor mv = this.classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "<clinit>", "()V", null, null);
163182
mv.visitCode();
164-
this.nextFreeVariableId = 0; // To 0 because there is no 'this' in a clinit
183+
this.nextFreeVariableId = 0; // to 0 because there is no 'this' in a clinit
165184
for (ClinitAdder clinitAdder : this.clinitAdders) {
166185
clinitAdder.generateCode(mv, this);
167186
}
168187
mv.visitInsn(RETURN);
169-
mv.visitMaxs(0,0); // not supplied due to COMPUTE_MAXS
188+
mv.visitMaxs(0,0); // not supplied due to COMPUTE_MAXS
170189
mv.visitEnd();
171190
}
172191
}
@@ -204,18 +223,18 @@ public int nextFreeVariableId() {
204223
}
205224

206225
public String getClassName() {
207-
return this.clazzName;
226+
return this.className;
208227
}
209228

210229
@Deprecated
211230
public String getClassname() {
212-
return this.clazzName;
231+
return this.className;
213232
}
214233

215234

216235
/**
217236
* Insert any necessary cast and value call to convert from a boxed type to a
218-
* primitive value
237+
* primitive value.
219238
* @param mv the method visitor into which instructions should be inserted
220239
* @param ch the primitive type desired as output
221240
* @param stackDescriptor the descriptor of the type on top of the stack

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

Lines changed: 25 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,8 @@
1616

1717
package org.springframework.expression.spel.ast;
1818

19-
import org.springframework.asm.Label;
2019
import org.springframework.asm.MethodVisitor;
20+
import org.springframework.expression.EvaluationContext;
2121
import org.springframework.expression.EvaluationException;
2222
import org.springframework.expression.spel.CodeFlow;
2323
import org.springframework.expression.spel.ExpressionState;
@@ -43,105 +43,52 @@ public BooleanTypedValue getValueInternal(ExpressionState state) throws Evaluati
4343
Object right = getRightOperand().getValueInternal(state).getValue();
4444
this.leftActualDescriptor = CodeFlow.toDescriptorFromObject(left);
4545
this.rightActualDescriptor = CodeFlow.toDescriptorFromObject(right);
46-
return BooleanTypedValue.forValue(equalityCheck(state, left, right));
46+
return BooleanTypedValue.forValue(
47+
equalityCheck(state.getEvaluationContext(), left, right));
4748
}
48-
49+
4950
// This check is different to the one in the other numeric operators (OpLt/etc)
5051
// because it allows for simple object comparison
5152
@Override
5253
public boolean isCompilable() {
5354
SpelNodeImpl left = getLeftOperand();
54-
SpelNodeImpl right= getRightOperand();
55+
SpelNodeImpl right = getRightOperand();
5556
if (!left.isCompilable() || !right.isCompilable()) {
5657
return false;
5758
}
5859

5960
String leftDesc = left.exitTypeDescriptor;
6061
String rightDesc = right.exitTypeDescriptor;
61-
DescriptorComparison dc = DescriptorComparison.checkNumericCompatibility(
62-
leftDesc, rightDesc, this.leftActualDescriptor, this.rightActualDescriptor);
62+
DescriptorComparison dc = DescriptorComparison.checkNumericCompatibility(leftDesc,
63+
rightDesc, this.leftActualDescriptor, this.rightActualDescriptor);
6364
return (!dc.areNumbers || dc.areCompatible);
6465
}
65-
66-
66+
6767
@Override
6868
public void generateCode(MethodVisitor mv, CodeFlow cf) {
69+
cf.loadEvaluationContext(mv);
6970
String leftDesc = getLeftOperand().exitTypeDescriptor;
7071
String rightDesc = getRightOperand().exitTypeDescriptor;
71-
Label elseTarget = new Label();
72-
Label endOfIf = new Label();
7372
boolean leftPrim = CodeFlow.isPrimitive(leftDesc);
7473
boolean rightPrim = CodeFlow.isPrimitive(rightDesc);
7574

76-
DescriptorComparison dc = DescriptorComparison.checkNumericCompatibility(
77-
leftDesc, rightDesc, this.leftActualDescriptor, this.rightActualDescriptor);
78-
79-
if (dc.areNumbers && dc.areCompatible) {
80-
char targetType = dc.compatibleType;
81-
getLeftOperand().generateCode(mv, cf);
82-
if (!leftPrim) {
83-
CodeFlow.insertUnboxInsns(mv, targetType, leftDesc);
84-
}
85-
cf.enterCompilationScope();
86-
getRightOperand().generateCode(mv, cf);
87-
cf.exitCompilationScope();
88-
if (!rightPrim) {
89-
CodeFlow.insertUnboxInsns(mv, targetType, rightDesc);
90-
}
91-
// assert: SpelCompiler.boxingCompatible(leftDesc, rightDesc)
92-
if (targetType == 'D') {
93-
mv.visitInsn(DCMPL);
94-
mv.visitJumpInsn(IFNE, elseTarget);
95-
}
96-
else if (targetType == 'F') {
97-
mv.visitInsn(FCMPL);
98-
mv.visitJumpInsn(IFNE, elseTarget);
99-
}
100-
else if (targetType == 'J') {
101-
mv.visitInsn(LCMP);
102-
mv.visitJumpInsn(IFNE, elseTarget);
103-
}
104-
else if (targetType == 'I' || targetType == 'Z') {
105-
mv.visitJumpInsn(IF_ICMPNE, elseTarget);
106-
}
107-
else {
108-
throw new IllegalStateException("Unexpected descriptor " + leftDesc);
109-
}
75+
cf.enterCompilationScope();
76+
getLeftOperand().generateCode(mv, cf);
77+
cf.exitCompilationScope();
78+
if (leftPrim) {
79+
CodeFlow.insertBoxIfNecessary(mv, leftDesc.charAt(0));
11080
}
111-
else {
112-
getLeftOperand().generateCode(mv, cf);
113-
if (leftPrim) {
114-
CodeFlow.insertBoxIfNecessary(mv, leftDesc.charAt(0));
115-
}
116-
getRightOperand().generateCode(mv, cf);
117-
if (rightPrim) {
118-
CodeFlow.insertBoxIfNecessary(mv, rightDesc.charAt(0));
119-
}
120-
Label leftNotNull = new Label();
121-
mv.visitInsn(DUP_X1); // dup right on the top of the stack
122-
mv.visitJumpInsn(IFNONNULL, leftNotNull);
123-
// Right is null!
124-
mv.visitInsn(SWAP);
125-
mv.visitInsn(POP); // remove it
126-
Label rightNotNull = new Label();
127-
mv.visitJumpInsn(IFNONNULL, rightNotNull);
128-
// Left is null too
129-
mv.visitInsn(ICONST_1);
130-
mv.visitJumpInsn(GOTO, endOfIf);
131-
mv.visitLabel(rightNotNull);
132-
mv.visitInsn(ICONST_0);
133-
mv.visitJumpInsn(GOTO, endOfIf);
134-
mv.visitLabel(leftNotNull);
135-
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "equals", "(Ljava/lang/Object;)Z", false);
136-
mv.visitLabel(endOfIf);
137-
cf.pushDescriptor("Z");
138-
return;
81+
cf.enterCompilationScope();
82+
getRightOperand().generateCode(mv, cf);
83+
cf.exitCompilationScope();
84+
if (rightPrim) {
85+
CodeFlow.insertBoxIfNecessary(mv, rightDesc.charAt(0));
13986
}
140-
mv.visitInsn(ICONST_1);
141-
mv.visitJumpInsn(GOTO, endOfIf);
142-
mv.visitLabel(elseTarget);
143-
mv.visitInsn(ICONST_0);
144-
mv.visitLabel(endOfIf);
87+
88+
String operatorClassName = Operator.class.getName().replace('.', '/');
89+
String evaluationContextClassName = EvaluationContext.class.getName().replace('.', '/');
90+
mv.visitMethodInsn(INVOKESTATIC, operatorClassName, "equalityCheck",
91+
"(L" + evaluationContextClassName + ";Ljava/lang/Object;Ljava/lang/Object;)Z", false);
14592
cf.pushDescriptor("Z");
14693
}
14794

0 commit comments

Comments
 (0)