Skip to content

Commit aae221c

Browse files
committed
Fix SpEL compilation of constructor invocation
The argument processing for compiling constructor references was very basic and this fix removes that and ensures the comprehensive logic written for method argument processing (under SPR-12328) is now used for both method and constructor argument handling. This fixes the reported issue and ensures varargs constructor references can be compiled. This also includes a couple of small fixes for the secondary testcase reported in SPR-12326. The first is to ensure the right root context object is used when it is passed to getValue() indirectly through the evaluation context. The final fix is to ensure correct boxing of primitives is done when a method is called upon a primitive. Issue: SPR-12326
1 parent c5e360d commit aae221c

File tree

7 files changed

+252
-21
lines changed

7 files changed

+252
-21
lines changed

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.expression.spel;
1818

1919
import java.lang.reflect.Constructor;
20+
import java.lang.reflect.Member;
2021
import java.lang.reflect.Method;
2122
import java.util.ArrayList;
2223
import java.util.List;
@@ -837,12 +838,23 @@ public static void insertNewArrayCode(MethodVisitor mv, int size, String arrayty
837838
* packaged into an array.
838839
* @param mv the method visitor where code should be generated
839840
* @param cf the current codeflow
840-
* @param method the method for which arguments are being setup
841+
* @param member the method or constructor for which arguments are being setup
841842
* @param arguments the expression nodes for the expression supplied argument values
842843
*/
843-
public static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Method method, SpelNodeImpl[] arguments) {
844-
String[] paramDescriptors = CodeFlow.toParamDescriptors(method);
845-
if (method.isVarArgs()) {
844+
public static void generateCodeForArguments(MethodVisitor mv, CodeFlow cf, Member member, SpelNodeImpl[] arguments) {
845+
String[] paramDescriptors = null;
846+
boolean isVarargs = false;
847+
if (member instanceof Constructor) {
848+
Constructor<?> ctor = (Constructor<?>)member;
849+
paramDescriptors = toDescriptors(ctor.getParameterTypes());
850+
isVarargs = ctor.isVarArgs();
851+
}
852+
else { // Method
853+
Method method = (Method)member;
854+
paramDescriptors = toDescriptors(method.getParameterTypes());
855+
isVarargs = method.isVarArgs();
856+
}
857+
if (isVarargs) {
846858
// The final parameter may or may not need packaging into an array, or nothing may
847859
// have been passed to satisfy the varargs and so something needs to be built.
848860
int p = 0; // Current supplied argument being processed

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

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -437,29 +437,21 @@ public boolean isCompilable() {
437437

438438
ReflectiveConstructorExecutor executor = (ReflectiveConstructorExecutor) this.cachedExecutor;
439439
Constructor<?> constructor = executor.getConstructor();
440-
return (!constructor.isVarArgs() && Modifier.isPublic(constructor.getModifiers()) &&
440+
return (Modifier.isPublic(constructor.getModifiers()) &&
441441
Modifier.isPublic(constructor.getDeclaringClass().getModifiers()));
442442
}
443443

444444
@Override
445445
public void generateCode(MethodVisitor mv, CodeFlow cf) {
446446
ReflectiveConstructorExecutor executor = ((ReflectiveConstructorExecutor) this.cachedExecutor);
447-
Constructor<?> constructor = executor.getConstructor();
448-
447+
Constructor<?> constructor = executor.getConstructor();
449448
String classSlashedDescriptor = constructor.getDeclaringClass().getName().replace('.', '/');
450-
String[] paramDescriptors = CodeFlow.toParamDescriptors(constructor);
451449
mv.visitTypeInsn(NEW, classSlashedDescriptor);
452450
mv.visitInsn(DUP);
453-
for (int c = 1; c < this.children.length; c++) { // children[0] is the type of the constructor
454-
SpelNodeImpl child = this.children[c];
455-
cf.enterCompilationScope();
456-
child.generateCode(mv, cf);
457-
// Check if need to box it for the method reference?
458-
if (CodeFlow.isPrimitive(cf.lastDescriptor()) && paramDescriptors[c-1].charAt(0) == 'L') {
459-
CodeFlow.insertBoxIfNecessary(mv, cf.lastDescriptor().charAt(0));
460-
}
461-
cf.exitCompilationScope();
462-
}
451+
// children[0] is the type of the constructor, don't want to include that in argument processing
452+
SpelNodeImpl[] arguments = new SpelNodeImpl[children.length-1];
453+
System.arraycopy(children, 1, arguments, 0, children.length-1);
454+
CodeFlow.generateCodeForArguments(mv, cf, constructor, arguments);
463455
mv.visitMethodInsn(INVOKESPECIAL, classSlashedDescriptor, "<init>",
464456
CodeFlow.createSignatureDescriptor(constructor), false);
465457
cf.pushDescriptor(this.exitTypeDescriptor);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) {
295295
if (descriptor == null && !isStaticMethod) {
296296
cf.loadTarget(mv);
297297
}
298+
299+
if (CodeFlow.isPrimitive(descriptor)) {
300+
CodeFlow.insertBoxIfNecessary(mv, descriptor.charAt(0));
301+
}
298302

299303
boolean itf = method.getDeclaringClass().isInterface();
300304
String methodDeclaringClassSlashedDescriptor = null;

spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ public Object getValue(EvaluationContext context) throws EvaluationException {
221221
Assert.notNull(context, "The EvaluationContext is required");
222222
if (compiledAst!= null) {
223223
try {
224-
Object result = this.compiledAst.getValue(null,context);
224+
TypedValue contextRoot = context == null ? null : context.getRootObject();
225+
Object result = this.compiledAst.getValue(contextRoot==null?null:contextRoot.getValue(),context);
225226
return result;
226227
}
227228
catch (Throwable ex) {
@@ -272,7 +273,8 @@ public Object getValue(EvaluationContext context, Object rootObject) throws Eval
272273
public <T> T getValue(EvaluationContext context, Class<T> expectedResultType) throws EvaluationException {
273274
if (this.compiledAst != null) {
274275
try {
275-
Object result = this.compiledAst.getValue(null,context);
276+
TypedValue contextRoot = context == null ? null : context.getRootObject();
277+
Object result = this.compiledAst.getValue(contextRoot==null?null:contextRoot.getValue(),context);
276278
if (expectedResultType != null) {
277279
return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType);
278280
}

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

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.expression.spel.standard.SpelExpression;
3939
import org.springframework.expression.spel.standard.SpelExpressionParser;
4040
import org.springframework.expression.spel.support.StandardEvaluationContext;
41+
import org.springframework.expression.spel.testdata.PersonInOtherPackage;
4142

4243
import static org.junit.Assert.*;
4344

@@ -1683,6 +1684,128 @@ public void opModulus_12041() throws Exception {
16831684
assertEquals(1.0f,expression.getValue());
16841685
}
16851686

1687+
@Test
1688+
public void constructorReference_SPR12326() {
1689+
String type = this.getClass().getName();
1690+
String prefix = "new "+type+".Obj";
1691+
1692+
expression = parser.parseExpression(prefix+"([0])");
1693+
assertEquals("test", ((Obj) expression.getValue(new Object[] { "test" })).param1);
1694+
assertCanCompile(expression);
1695+
assertEquals("test", ((Obj) expression.getValue(new Object[] { "test" })).param1);
1696+
1697+
expression = parser.parseExpression(prefix+"2('foo','bar').output");
1698+
assertEquals("foobar", expression.getValue(String.class));
1699+
assertCanCompile(expression);
1700+
assertEquals("foobar", expression.getValue(String.class));
1701+
1702+
expression = parser.parseExpression(prefix+"2('foo').output");
1703+
assertEquals("foo", expression.getValue(String.class));
1704+
assertCanCompile(expression);
1705+
assertEquals("foo", expression.getValue(String.class));
1706+
1707+
expression = parser.parseExpression(prefix+"2().output");
1708+
assertEquals("", expression.getValue(String.class));
1709+
assertCanCompile(expression);
1710+
assertEquals("", expression.getValue(String.class));
1711+
1712+
expression = parser.parseExpression(prefix+"3(1,2,3).output");
1713+
assertEquals("123", expression.getValue(String.class));
1714+
assertCanCompile(expression);
1715+
assertEquals("123", expression.getValue(String.class));
1716+
1717+
expression = parser.parseExpression(prefix+"3(1).output");
1718+
assertEquals("1", expression.getValue(String.class));
1719+
assertCanCompile(expression);
1720+
assertEquals("1", expression.getValue(String.class));
1721+
1722+
expression = parser.parseExpression(prefix+"3().output");
1723+
assertEquals("", expression.getValue(String.class));
1724+
assertCanCompile(expression);
1725+
assertEquals("", expression.getValue(String.class));
1726+
1727+
expression = parser.parseExpression(prefix+"3('abc',5.0f,1,2,3).output");
1728+
assertEquals("abc:5.0:123", expression.getValue(String.class));
1729+
assertCanCompile(expression);
1730+
assertEquals("abc:5.0:123", expression.getValue(String.class));
1731+
1732+
expression = parser.parseExpression(prefix+"3('abc',5.0f,1).output");
1733+
assertEquals("abc:5.0:1", expression.getValue(String.class));
1734+
assertCanCompile(expression);
1735+
assertEquals("abc:5.0:1", expression.getValue(String.class));
1736+
1737+
expression = parser.parseExpression(prefix+"3('abc',5.0f).output");
1738+
assertEquals("abc:5.0:", expression.getValue(String.class));
1739+
assertCanCompile(expression);
1740+
assertEquals("abc:5.0:", expression.getValue(String.class));
1741+
1742+
expression = parser.parseExpression(prefix+"4(#root).output");
1743+
assertEquals("123", expression.getValue(new int[]{1,2,3},String.class));
1744+
assertCanCompile(expression);
1745+
assertEquals("123", expression.getValue(new int[]{1,2,3},String.class));
1746+
}
1747+
1748+
@Test
1749+
public void methodReferenceMissingCastAndRootObjectAccessing_SPR12326() {
1750+
// Need boxing code on the 1 so that toString() can be called
1751+
expression = parser.parseExpression("1.toString()");
1752+
assertEquals("1", expression.getValue());
1753+
assertCanCompile(expression);
1754+
assertEquals("1", expression.getValue());
1755+
1756+
expression = parser.parseExpression("#it?.age.equals([0])");
1757+
Person person = new Person(1);
1758+
StandardEvaluationContext context =
1759+
new StandardEvaluationContext(new Object[] { person.getAge() });
1760+
context.setVariable("it", person);
1761+
assertTrue(expression.getValue(context, Boolean.class));
1762+
assertCanCompile(expression);
1763+
assertTrue(expression.getValue(context, Boolean.class));
1764+
1765+
// Variant of above more like what was in the bug report:
1766+
SpelExpressionParser parser = new SpelExpressionParser(
1767+
new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
1768+
this.getClass().getClassLoader()));
1769+
1770+
SpelExpression ex = parser.parseRaw("#it?.age.equals([0])");
1771+
context = new StandardEvaluationContext(new Object[] { person.getAge() });
1772+
context.setVariable("it", person);
1773+
assertTrue(ex.getValue(context, Boolean.class));
1774+
assertTrue(ex.getValue(context, Boolean.class));
1775+
1776+
PersonInOtherPackage person2 = new PersonInOtherPackage(1);
1777+
ex = parser.parseRaw("#it?.age.equals([0])");
1778+
context =
1779+
new StandardEvaluationContext(new Object[] { person2.getAge() });
1780+
context.setVariable("it", person2);
1781+
assertTrue(ex.getValue(context, Boolean.class));
1782+
assertTrue(ex.getValue(context, Boolean.class));
1783+
1784+
ex = parser.parseRaw("#it?.age.equals([0])");
1785+
context =
1786+
new StandardEvaluationContext(new Object[] { person2.getAge() });
1787+
context.setVariable("it", person2);
1788+
assertTrue((Boolean)ex.getValue(context));
1789+
assertTrue((Boolean)ex.getValue(context));
1790+
}
1791+
1792+
public class Person {
1793+
1794+
private int age;
1795+
1796+
public Person(int age) {
1797+
this.age = age;
1798+
}
1799+
1800+
public int getAge() {
1801+
return age;
1802+
}
1803+
1804+
public void setAge(int age) {
1805+
this.age = age;
1806+
}
1807+
}
1808+
16861809
@Test
16871810
public void constructorReference() throws Exception {
16881811
// simple ctor
@@ -3440,6 +3563,66 @@ private TestClass8(String a, String b) {
34403563
this.s = a+b;
34413564
}
34423565
}
3566+
3567+
public static class Obj {
3568+
3569+
private final String param1;
3570+
3571+
public Obj(String param1){
3572+
this.param1 = param1;
3573+
}
3574+
}
3575+
3576+
public static class Obj2 {
3577+
3578+
public final String output;
3579+
3580+
public Obj2(String... params){
3581+
StringBuilder b = new StringBuilder();
3582+
for (String param: params) {
3583+
b.append(param);
3584+
}
3585+
output = b.toString();
3586+
}
3587+
}
3588+
3589+
public static class Obj3 {
3590+
3591+
public final String output;
3592+
3593+
public Obj3(int... params) {
3594+
StringBuilder b = new StringBuilder();
3595+
for (int param: params) {
3596+
b.append(Integer.toString(param));
3597+
}
3598+
output = b.toString();
3599+
}
3600+
3601+
public Obj3(String s, Float f, int... ints) {
3602+
StringBuilder b = new StringBuilder();
3603+
b.append(s);
3604+
b.append(":");
3605+
b.append(Float.toString(f));
3606+
b.append(":");
3607+
for (int param: ints) {
3608+
b.append(Integer.toString(param));
3609+
}
3610+
output = b.toString();
3611+
}
3612+
}
3613+
3614+
public static class Obj4 {
3615+
3616+
public final String output;
3617+
3618+
public Obj4(int[] params) {
3619+
StringBuilder b = new StringBuilder();
3620+
for (int param: params) {
3621+
b.append(Integer.toString(param));
3622+
}
3623+
output = b.toString();
3624+
}
3625+
}
34433626

34443627
@SuppressWarnings("unused")
34453628
private static class TestClass9 {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ public void stringConcatenation() throws Exception {
156156
Object o = expression.getValue(g);
157157
assertEquals("helloworld spring", o);
158158

159-
System.out.println("Performance check for SpEL expression: '{'abcde','ijklm'}[0].substring({1,3,4}[0],{1,3,4}[1])'");
159+
System.out.println("Performance check for SpEL expression: 'hello' + getWorld() + ' spring'");
160160
long stime = System.currentTimeMillis();
161161
for (int i=0;i<1000000;i++) {
162162
o = expression.getValue(g);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.expression.spel.testdata;
17+
18+
/**
19+
*
20+
* @author Andy Clement
21+
* @since 4.1.2
22+
*/
23+
public class PersonInOtherPackage {
24+
25+
private int age;
26+
27+
public PersonInOtherPackage(int age) {
28+
this.age = age;
29+
}
30+
31+
public int getAge() {
32+
return age;
33+
}
34+
35+
public void setAge(int age) {
36+
this.age = age;
37+
}
38+
}

0 commit comments

Comments
 (0)