From 2eeb2e92359381328789585233e54c9bbd41e6dc Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Wed, 4 Jun 2014 10:09:02 -0700 Subject: [PATCH] Add a compiler for SpEL With these changes an optional compiler is added for SpEL expressions. The compiler is off by default but can be enabled via the SpEL parser configuration object or system property (when SpEL is embedded and parser configuration is not possible). Not all expressions are currently handled but the common cases are and it is an extensible compilation framework. Issue: SPR-10943 --- .../CompilablePropertyAccessor.java | 53 + .../expression/spel/CompiledExpression.java | 38 + .../expression/spel/SpelCompilerMode.java | 45 + .../expression/spel/SpelMessage.java | 5 +- .../spel/SpelParserConfiguration.java | 33 +- .../expression/spel/ast/BooleanLiteral.java | 21 +- .../spel/ast/CompoundExpression.java | 37 +- .../spel/ast/ConstructorReference.java | 59 +- .../expression/spel/ast/Elvis.java | 73 +- .../expression/spel/ast/FloatLiteral.java | 17 +- .../spel/ast/FunctionReference.java | 45 +- .../expression/spel/ast/Indexer.java | 160 +- .../expression/spel/ast/IntLiteral.java | 27 +- .../expression/spel/ast/LongLiteral.java | 17 +- .../expression/spel/ast/MethodReference.java | 97 +- .../expression/spel/ast/NullLiteral.java | 16 +- .../expression/spel/ast/OpAnd.java | 39 +- .../expression/spel/ast/OpDivide.java | 74 +- .../expression/spel/ast/OpEQ.java | 99 + .../expression/spel/ast/OpGE.java | 16 +- .../expression/spel/ast/OpGT.java | 13 +- .../expression/spel/ast/OpLE.java | 16 +- .../expression/spel/ast/OpLT.java | 14 +- .../expression/spel/ast/OpMinus.java | 87 +- .../expression/spel/ast/OpMultiply.java | 67 +- .../expression/spel/ast/OpNE.java | 79 + .../expression/spel/ast/OpOr.java | 39 +- .../expression/spel/ast/OpPlus.java | 69 +- .../expression/spel/ast/Operator.java | 72 + .../spel/ast/OperatorInstanceof.java | 32 +- .../expression/spel/ast/OperatorNot.java | 27 +- .../spel/ast/PropertyOrFieldReference.java | 40 +- .../expression/spel/ast/RealLiteral.java | 17 +- .../expression/spel/ast/SpelNodeImpl.java | 42 +- .../expression/spel/ast/StringLiteral.java | 15 +- .../expression/spel/ast/Ternary.java | 75 +- .../expression/spel/ast/TypeReference.java | 45 +- .../spel/ast/VariableReference.java | 27 +- .../expression/spel/standard/CodeFlow.java | 603 ++++ .../spel/standard/SpelCompiler.java | 277 ++ .../spel/standard/SpelExpression.java | 254 +- .../spel/support/ReflectionHelper.java | 19 +- .../ReflectiveConstructorExecutor.java | 8 +- .../support/ReflectiveMethodExecutor.java | 15 +- .../support/ReflectivePropertyAccessor.java | 57 +- .../spel/SpelCompilationCoverageTests.java | 2603 +++++++++++++++++ .../spel/SpelCompilationPerformanceTests.java | 471 +++ 47 files changed, 5964 insertions(+), 90 deletions(-) create mode 100644 spring-expression/src/main/java/org/springframework/expression/CompilablePropertyAccessor.java create mode 100644 spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java create mode 100644 spring-expression/src/main/java/org/springframework/expression/spel/SpelCompilerMode.java create mode 100644 spring-expression/src/main/java/org/springframework/expression/spel/standard/CodeFlow.java create mode 100644 spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java create mode 100644 spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationPerformanceTests.java diff --git a/spring-expression/src/main/java/org/springframework/expression/CompilablePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/CompilablePropertyAccessor.java new file mode 100644 index 00000000000..35caa15700b --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/CompilablePropertyAccessor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression; + +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.expression.spel.ast.PropertyOrFieldReference; +import org.springframework.expression.spel.standard.CodeFlow; + + +/** + * A compilable property accessor is able to generate bytecode that represents + * the access operation, facilitating compilation to bytecode of expressions + * that use the accessor. + * + * @author Andy Clement + * @since 4.1 + */ +public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes { + + /** + * @return true if this property accessor is currently suitable for compilation. + */ + boolean isCompilable(); + + /** + * Generate the bytecode the performs the access operation into the specified MethodVisitor using + * context information from the codeflow where necessary. + * @param propertyReference the property reference for which code is being generated + * @param mv the Asm method visitor into which code should be generated + * @param codeflow the current state of the expression compiler + */ + void generateCode(PropertyOrFieldReference propertyReference, MethodVisitor mv, CodeFlow codeflow); + + /** + * @return the type of the accessed property - may only be known once an access has occurred. + */ + Class getPropertyType(); +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java new file mode 100644 index 00000000000..6a2f88e6975 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression.spel; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; + +/** + * Base superclass for compiled expressions. Each generated compiled expression class will + * extend this class and implement one of the getValue() methods. It is not intended + * to subclassed by user code. + * + * @author Andy Clement + * @since 4.1 + */ +public abstract class CompiledExpression { + + /** + * Subclasses of CompiledExpression generated by SpelCompiler will provide an implementation of + * this method. + */ + public abstract Object getValue(Object target, EvaluationContext context) throws EvaluationException; + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelCompilerMode.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelCompilerMode.java new file mode 100644 index 00000000000..da53d058dee --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelCompilerMode.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression.spel; + +/** + * Captures the possible configuration settings for a compiler that can be + * used when evaluating expressions. + * + * @author Andy Clement + * @since 4.1 + */ +public enum SpelCompilerMode { + /** + * The compiler is switched off, this is the default. + */ + off, + + /** + * In immediate mode, expressions are compiled as soon as possible (usually after 1 interpreted run). + * If a compiled expression fails it will throw an exception to the caller. + */ + immediate, + + /** + * In mixed mode, expression evaluate silently switches between interpreted and compiled over time. + * After a number of runs the expression gets compiled. If it later fails (possibly due to inferred + * type information changing) then that will be caught internally and the system switches back to + * interpreted mode. It may subsequently compile it again later. + */ + mixed +} \ No newline at end of file diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index 834f63b55ab..b1e4c12c47d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -249,7 +249,10 @@ public enum SpelMessage { "Problem parsing left operand"), MISSING_SELECTION_EXPRESSION(Kind.ERROR, 1071, - "A required selection expression has not been specified"); + "A required selection expression has not been specified"), + + EXCEPTION_RUNNING_COMPILED_EXPRESSION(Kind.ERROR,1072, + "An exception occurred whilst evaluating a compiled expression"); private final Kind kind; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java index a4620901a88..7d5b4e2be22 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,15 @@ package org.springframework.expression.spel; +import org.springframework.core.SpringProperties; + + /** * Configuration object for the SpEL expression parser. * * @author Juergen Hoeller * @author Phillip Webb + * @author Andy Clement * @since 3.0 * @see org.springframework.expression.spel.standard.SpelExpressionParser#SpelExpressionParser(SpelParserConfiguration) */ @@ -29,10 +33,21 @@ public class SpelParserConfiguration { private final boolean autoGrowNullReferences; private final boolean autoGrowCollections; + + private static SpelCompilerMode defaultCompilerMode = SpelCompilerMode.off; + + private SpelCompilerMode compilerMode; private final int maximumAutoGrowSize; - + static { + String compilerMode = SpringProperties.getProperty("spring.expression.compiler.mode"); + if (compilerMode != null) { + defaultCompilerMode = SpelCompilerMode.valueOf(compilerMode.toLowerCase()); + // System.out.println("SpelCompiler: switched to "+defaultCompilerMode+" mode"); + } + } + /** * Create a new {@link SpelParserConfiguration} instance. * @param autoGrowNullReferences if null references should automatically grow @@ -53,8 +68,22 @@ public class SpelParserConfiguration { this.autoGrowNullReferences = autoGrowNullReferences; this.autoGrowCollections = autoGrowCollections; this.maximumAutoGrowSize = maximumAutoGrowSize; + this.compilerMode = defaultCompilerMode; + } + + /** + * @param compilerMode the compiler mode that parsers using this configuration object should use + */ + public void setCompilerMode(SpelCompilerMode compilerMode) { + this.compilerMode = compilerMode; } + /** + * @return the configuration mode for parsers using this configuration object + */ + public SpelCompilerMode getCompilerMode() { + return this.compilerMode; + } /** * @return {@code true} if {@code null} references should be automatically grown diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/BooleanLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/BooleanLiteral.java index 5aa748c8c06..abd9f89a827 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/BooleanLiteral.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/BooleanLiteral.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,8 @@ */ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; /** @@ -31,6 +33,7 @@ public class BooleanLiteral extends Literal { public BooleanLiteral(String payload, int pos, boolean value) { super(payload, pos); this.value = BooleanTypedValue.forValue(value); + this.exitTypeDescriptor = "Z"; } @@ -38,5 +41,21 @@ public class BooleanLiteral extends Literal { public BooleanTypedValue getLiteralValue() { return this.value; } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + if (this.value == BooleanTypedValue.TRUE) { + mv.visitLdcInsn(1); + } + else { + mv.visitLdcInsn(0); + } + codeflow.pushDescriptor(getExitDescriptor()); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java index 1ea345b582d..7973fe457bf 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java @@ -16,10 +16,12 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.standard.CodeFlow; /** * Represents a DOT separated expression sequence, such as 'property1.property2.methodOne()' @@ -81,7 +83,15 @@ public class CompoundExpression extends SpelNodeImpl { */ @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { - return getValueRef(state).getValue(); + ValueRef ref = getValueRef(state); + TypedValue result = ref.getValue(); + this.exitTypeDescriptor = this.children[this.children.length-1].getExitDescriptor(); + return result; + } + + @Override + public String getExitDescriptor() { + return this.exitTypeDescriptor; } @Override @@ -105,5 +115,30 @@ public class CompoundExpression extends SpelNodeImpl { } return sb.toString(); } + + @Override + public boolean isCompilable() { + for (SpelNodeImpl child: children) { + if (!child.isCompilable()) { + return false; + } + } + return true; + } + + @Override + public void generateCode(MethodVisitor mv,CodeFlow codeflow) { + // TODO could optimize T(SomeType).staticMethod - no need to generate the T() part + for (int i=0;i 1; } + + @Override + public boolean isCompilable() { + if (!(this.cachedExecutor instanceof ReflectiveConstructorExecutor) || + this.exitTypeDescriptor == null) { + return false; + } + if (getChildCount() > 1) { + for (int c = 1, max = getChildCount();c < max; c++) { + if (!children[c].isCompilable()) { + return false; + } + } + } + ReflectiveConstructorExecutor executor = (ReflectiveConstructorExecutor)this.cachedExecutor; + Constructor constructor = executor.getConstructor(); + if (!Modifier.isPublic(constructor.getModifiers()) || + !Modifier.isPublic(constructor.getDeclaringClass().getModifiers())) { + return false; + } + if (constructor.isVarArgs()) { + return false; + } + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + ReflectiveConstructorExecutor executor = ((ReflectiveConstructorExecutor) this.cachedExecutor); + Constructor constructor = executor.getConstructor(); + + String classSlashedDescriptor = constructor.getDeclaringClass().getName().replace('.','/'); + String[] paramDescriptors = CodeFlow.toParamDescriptors(constructor); + mv.visitTypeInsn(NEW,classSlashedDescriptor); + mv.visitInsn(DUP); + for (int c = 1; c < children.length; c++) { // children[0] is the type of the constructor + SpelNodeImpl child = children[c]; + codeflow.enterCompilationScope(); + child.generateCode(mv, codeflow); + // Check if need to box it for the method reference? + if (CodeFlow.isPrimitive(codeflow.lastDescriptor()) && (paramDescriptors[c-1].charAt(0)=='L')) { + CodeFlow.insertBoxIfNecessary(mv, codeflow.lastDescriptor().charAt(0)); + } + codeflow.exitCompilationScope(); + } + mv.visitMethodInsn(INVOKESPECIAL,classSlashedDescriptor,"",CodeFlow.createSignatureDescriptor(constructor),false); + codeflow.pushDescriptor(exitTypeDescriptor); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java index 7c44d0635cc..5677fcd7327 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,9 +16,12 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; /** * Represents the elvis operator ?:. For an expression "a?:b" if a is not null, the value @@ -49,7 +52,18 @@ public class Elvis extends SpelNodeImpl { return value; } else { - return this.children[1].getValueInternal(state); + TypedValue result = this.children[1].getValueInternal(state); + if (exitTypeDescriptor == null) { + String testDescriptor = this.children[0].exitTypeDescriptor; + String ifNullDescriptor = this.children[1].exitTypeDescriptor; + if (testDescriptor.equals(ifNullDescriptor)) { + this.exitTypeDescriptor = testDescriptor; + } + else { + this.exitTypeDescriptor = "Ljava/lang/Object"; + } + } + return result; } } @@ -59,4 +73,59 @@ public class Elvis extends SpelNodeImpl { getChild(1).toStringAST()).toString(); } + private void computeExitTypeDescriptor() { + if (exitTypeDescriptor == null && + this.children[0].getExitDescriptor()!=null && + this.children[1].getExitDescriptor()!=null) { + String conditionDescriptor = this.children[0].exitTypeDescriptor; + String ifNullValueDescriptor = this.children[1].exitTypeDescriptor; + if (conditionDescriptor.equals(ifNullValueDescriptor)) { + this.exitTypeDescriptor = conditionDescriptor; + } + else if (conditionDescriptor.equals("Ljava/lang/Object") && !CodeFlow.isPrimitive(ifNullValueDescriptor)) { + this.exitTypeDescriptor = ifNullValueDescriptor; + } + else if (ifNullValueDescriptor.equals("Ljava/lang/Object") && !CodeFlow.isPrimitive(conditionDescriptor)) { + this.exitTypeDescriptor = conditionDescriptor; + } + else { + // Use the easiest to compute common super type + this.exitTypeDescriptor = "Ljava/lang/Object"; + } + } + } + + @Override + public boolean isCompilable() { + SpelNodeImpl condition = this.children[0]; + SpelNodeImpl ifNullValue = this.children[1]; + if (!(condition.isCompilable() && ifNullValue.isCompilable())) { + return false; + } + return + condition.getExitDescriptor()!=null && + ifNullValue.getExitDescriptor()!=null; + } + + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + // exit type descriptor can be null if both components are literal expressions + computeExitTypeDescriptor(); + this.children[0].generateCode(mv, codeflow); + Label elseTarget = new Label(); + Label endOfIf = new Label(); + mv.visitInsn(DUP); + mv.visitJumpInsn(IFNULL, elseTarget); + mv.visitJumpInsn(GOTO, endOfIf); + mv.visitLabel(elseTarget); + mv.visitInsn(POP); + this.children[1].generateCode(mv, codeflow); + if (!CodeFlow.isPrimitive(getExitDescriptor())) { + CodeFlow.insertBoxIfNecessary(mv, codeflow.lastDescriptor().charAt(0)); + } + mv.visitLabel(endOfIf); + codeflow.pushDescriptor(getExitDescriptor()); + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FloatLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FloatLiteral.java index 6d7be2b4815..e11bbf37da8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FloatLiteral.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FloatLiteral.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.CodeFlow; /** * Expression language AST node that represents a float literal. * * @author Satyapal Reddy + * @author Andy Clement * @since 3.2 */ public class FloatLiteral extends Literal { @@ -31,6 +34,7 @@ public class FloatLiteral extends Literal { FloatLiteral(String payload, int pos, float value) { super(payload, pos); this.value = new TypedValue(value); + this.exitTypeDescriptor = "F"; } @@ -38,4 +42,15 @@ public class FloatLiteral extends Literal { public TypedValue getLiteralValue() { return this.value; } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + mv.visitLdcInsn(this.value.getValue()); + codeflow.pushDescriptor(getExitDescriptor()); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java index 642c4497ee2..f1e39a6e132 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.expression.spel.ast; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import org.springframework.asm.MethodVisitor; import org.springframework.core.MethodParameter; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationException; @@ -27,6 +28,7 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.ReflectionHelper; import org.springframework.util.ReflectionUtils; @@ -48,6 +50,10 @@ public class FunctionReference extends SpelNodeImpl { private final String name; + // Captures the most recently used method for the function invocation *if* + // the method can safely be used for compilation (i.e. no argument conversion is + // going on) + private Method method; public FunctionReference(String functionName, int pos, SpelNodeImpl... arguments) { super(pos,arguments); @@ -84,6 +90,7 @@ public class FunctionReference extends SpelNodeImpl { * @throws EvaluationException if there is any problem invoking the method */ private TypedValue executeFunctionJLRMethod(ExpressionState state, Method method) throws EvaluationException { + this.method = null; Object[] functionArgs = getArguments(state); if (!method.isVarArgs() && method.getParameterTypes().length != functionArgs.length) { @@ -96,11 +103,11 @@ public class FunctionReference extends SpelNodeImpl { SpelMessage.FUNCTION_MUST_BE_STATIC, method.getDeclaringClass().getName() + "." + method.getName(), this.name); } - + boolean argumentConversionOccurred = false; // Convert arguments if necessary and remap them for varargs if required if (functionArgs != null) { TypeConverter converter = state.getEvaluationContext().getTypeConverter(); - ReflectionHelper.convertAllArguments(converter, functionArgs, method); + argumentConversionOccurred |= ReflectionHelper.convertAllArguments(converter, functionArgs, method); } if (method.isVarArgs()) { functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation( @@ -110,6 +117,10 @@ public class FunctionReference extends SpelNodeImpl { try { ReflectionUtils.makeAccessible(method); Object result = method.invoke(method.getClass(), functionArgs); + if (!argumentConversionOccurred) { + this.method = method; + this.exitTypeDescriptor = CodeFlow.toDescriptor(method.getReturnType()); + } return new TypedValue(result, new TypeDescriptor(new MethodParameter(method,-1)).narrow(result)); } catch (Exception ex) { @@ -146,5 +157,33 @@ public class FunctionReference extends SpelNodeImpl { } return arguments; } + + @Override + public boolean isCompilable() { + // Don't yet support non-static method compilation. + return method!=null && Modifier.isStatic(method.getModifiers()); + } + + @Override + public void generateCode(MethodVisitor mv,CodeFlow codeflow) { + String methodDeclaringClassSlashedDescriptor = method.getDeclaringClass().getName().replace('.','/'); + String[] paramDescriptors = CodeFlow.toParamDescriptors(method); + for (int c = 0; c < children.length; c++) { + SpelNodeImpl child = children[c]; + codeflow.enterCompilationScope(); + child.generateCode(mv, codeflow); + // Check if need to box it for the method reference? + if (CodeFlow.isPrimitive(codeflow.lastDescriptor()) && (paramDescriptors[c].charAt(0)=='L')) { + CodeFlow.insertBoxIfNecessary(mv, codeflow.lastDescriptor().charAt(0)); + } + else if (!codeflow.lastDescriptor().equals(paramDescriptors[c])) { + // This would be unnecessary in the case of subtyping (e.g. method takes a Number but passed in is an Integer) + CodeFlow.insertCheckCast(mv, paramDescriptors[c]); + } + codeflow.exitCompilationScope(); + } + mv.visitMethodInsn(INVOKESTATIC,methodDeclaringClassSlashedDescriptor,method.getName(),CodeFlow.createSignatureDescriptor(method),false); + codeflow.pushDescriptor(exitTypeDescriptor); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index a65f3d5e02f..ad5774a6d3c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -16,10 +16,15 @@ package org.springframework.expression.spel.ast; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Collection; import java.util.List; import java.util.Map; +import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; @@ -30,6 +35,7 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.ReflectivePropertyAccessor; /** @@ -66,6 +72,10 @@ public class Indexer extends SpelNodeImpl { private PropertyAccessor cachedWriteAccessor; + private static enum IndexedType { map, array, list, string, object; } + + private IndexedType indexedType; + public Indexer(int pos, SpelNodeImpl expr) { super(pos, expr); @@ -122,6 +132,7 @@ public class Indexer extends SpelNodeImpl { if (targetObjectTypeDescriptor.getMapKeyTypeDescriptor() != null) { key = state.convertValue(key, targetObjectTypeDescriptor.getMapKeyTypeDescriptor()); } + this.indexedType = IndexedType.map; return new MapIndexingValueRef(state.getTypeConverter(), (Map) targetObject, key, targetObjectTypeDescriptor); } @@ -135,14 +146,19 @@ public class Indexer extends SpelNodeImpl { if (targetObject.getClass().isArray() || targetObject instanceof Collection || targetObject instanceof String) { int idx = (Integer) state.convertValue(index, TypeDescriptor.valueOf(Integer.class)); if (targetObject.getClass().isArray()) { + this.indexedType = IndexedType.array; return new ArrayIndexingValueRef(state.getTypeConverter(), targetObject, idx, targetObjectTypeDescriptor); } else if (targetObject instanceof Collection) { + if (targetObject instanceof List) { + this.indexedType = IndexedType.list; + } return new CollectionIndexingValueRef((Collection) targetObject, idx, targetObjectTypeDescriptor, state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(), state.getConfiguration().getMaximumAutoGrowSize()); } else if (targetObject instanceof String) { + this.indexedType = IndexedType.string; return new StringIndexingLValue((String) targetObject, idx, targetObjectTypeDescriptor); } } @@ -150,6 +166,7 @@ public class Indexer extends SpelNodeImpl { // Try and treat the index value as a property of the context object // TODO could call the conversion service to convert the value to a String if (indexValue.getTypeDescriptor().getType() == String.class) { + this.indexedType = IndexedType.object; return new PropertyIndexingValueRef(targetObject, (String) indexValue.getValue(), state.getEvaluationContext(), targetObjectTypeDescriptor); } @@ -157,6 +174,121 @@ public class Indexer extends SpelNodeImpl { throw new SpelEvaluationException(getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetObjectTypeDescriptor.toString()); } + + @Override + public boolean isCompilable() { + if (this.indexedType == IndexedType.array) { + return exitTypeDescriptor != null; + } + else if (this.indexedType == IndexedType.list || this.indexedType == IndexedType.map) { + return true; + } + else if (this.indexedType == IndexedType.object) { + // If the string name is changing the accessor is clearly going to change (so compilation is not possible) + if (this.cachedReadAccessor != null && + (this.cachedReadAccessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor) && + (getChild(0) instanceof StringLiteral)) { + return true; + }; + } + return false; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + String s = codeflow.lastDescriptor(); + + if (s == null) { + // stack is empty, should use context object + codeflow.loadTarget(mv); + } + + if (this.indexedType == IndexedType.array) { + if (exitTypeDescriptor == "I") { + mv.visitTypeInsn(CHECKCAST,"[I"); + SpelNodeImpl index = this.children[0]; + index.generateCode(mv, codeflow); + mv.visitInsn(IALOAD); + } + else if (exitTypeDescriptor == "D") { + mv.visitTypeInsn(CHECKCAST,"[D"); + SpelNodeImpl index = this.children[0]; + index.generateCode(mv, codeflow); + mv.visitInsn(DALOAD); + } + else if (exitTypeDescriptor == "J") { + mv.visitTypeInsn(CHECKCAST,"[J"); + SpelNodeImpl index = this.children[0]; + index.generateCode(mv, codeflow); + mv.visitInsn(LALOAD); + } + else if (exitTypeDescriptor == "F") { + mv.visitTypeInsn(CHECKCAST,"[F"); + SpelNodeImpl index = this.children[0]; + index.generateCode(mv, codeflow); + mv.visitInsn(FALOAD); + } + else if (exitTypeDescriptor == "S") { + mv.visitTypeInsn(CHECKCAST,"[S"); + SpelNodeImpl index = this.children[0]; + index.generateCode(mv, codeflow); + mv.visitInsn(SALOAD); + } + else if (exitTypeDescriptor == "B") { + mv.visitTypeInsn(CHECKCAST,"[B"); + SpelNodeImpl index = this.children[0]; + index.generateCode(mv, codeflow); + mv.visitInsn(BALOAD); + } + else if (exitTypeDescriptor == "C") { + mv.visitTypeInsn(CHECKCAST,"[C"); + SpelNodeImpl index = this.children[0]; + index.generateCode(mv, codeflow); + mv.visitInsn(CALOAD); + } + else { + mv.visitTypeInsn(CHECKCAST,"["+exitTypeDescriptor+(CodeFlow.isPrimitiveArray(exitTypeDescriptor)?"":";"));//depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;"); + SpelNodeImpl index = this.children[0]; + index.generateCode(mv, codeflow); + mv.visitInsn(AALOAD); + } + } + else if (this.indexedType == IndexedType.list) { + mv.visitTypeInsn(CHECKCAST,"java/util/List"); + this.children[0].generateCode(mv, codeflow); + mv.visitMethodInsn(INVOKEINTERFACE,"java/util/List","get","(I)Ljava/lang/Object;", true); + CodeFlow.insertCheckCast(mv,exitTypeDescriptor); + } + else if (this.indexedType == IndexedType.map) { + mv.visitTypeInsn(CHECKCAST,"java/util/Map"); + this.children[0].generateCode(mv, codeflow); + mv.visitMethodInsn(INVOKEINTERFACE,"java/util/Map","get","(Ljava/lang/Object;)Ljava/lang/Object;", true); + CodeFlow.insertCheckCast(mv,exitTypeDescriptor); + } + else if (this.indexedType == IndexedType.object) { + ReflectivePropertyAccessor.OptimalPropertyAccessor accessor = + (ReflectivePropertyAccessor.OptimalPropertyAccessor)this.cachedReadAccessor; + Member member = accessor.member; + boolean isStatic = Modifier.isStatic(member.getModifiers()); + + String descriptor = codeflow.lastDescriptor(); + String memberDeclaringClassSlashedDescriptor = member.getDeclaringClass().getName().replace('.','/'); + if (!isStatic) { + if (descriptor == null) { + codeflow.loadTarget(mv); + } + if (descriptor == null || !memberDeclaringClassSlashedDescriptor.equals(descriptor.substring(1))) { + mv.visitTypeInsn(CHECKCAST, memberDeclaringClassSlashedDescriptor); + } + } + if (member instanceof Field) { + mv.visitFieldInsn(isStatic?GETSTATIC:GETFIELD,memberDeclaringClassSlashedDescriptor,member.getName(),CodeFlow.toJVMDescriptor(((Field) member).getType())); + } else { + mv.visitMethodInsn(isStatic?INVOKESTATIC:INVOKEVIRTUAL, memberDeclaringClassSlashedDescriptor, member.getName(),CodeFlow.createSignatureDescriptor((Method)member),false); + } + } + codeflow.pushDescriptor(exitTypeDescriptor); + } @Override public String toStringAST() { @@ -236,47 +368,57 @@ public class Indexer extends SpelNodeImpl { if (arrayComponentType == Integer.TYPE) { int[] array = (int[]) ctx; checkAccess(array.length, idx); + this.exitTypeDescriptor = "I"; return array[idx]; } else if (arrayComponentType == Boolean.TYPE) { boolean[] array = (boolean[]) ctx; checkAccess(array.length, idx); + this.exitTypeDescriptor = "Z"; return array[idx]; } else if (arrayComponentType == Character.TYPE) { char[] array = (char[]) ctx; checkAccess(array.length, idx); + this.exitTypeDescriptor = "C"; return array[idx]; } else if (arrayComponentType == Long.TYPE) { long[] array = (long[]) ctx; checkAccess(array.length, idx); + this.exitTypeDescriptor = "J"; return array[idx]; } else if (arrayComponentType == Short.TYPE) { short[] array = (short[]) ctx; checkAccess(array.length, idx); + this.exitTypeDescriptor = "S"; return array[idx]; } else if (arrayComponentType == Double.TYPE) { double[] array = (double[]) ctx; checkAccess(array.length, idx); + this.exitTypeDescriptor = "D"; return array[idx]; } else if (arrayComponentType == Float.TYPE) { float[] array = (float[]) ctx; checkAccess(array.length, idx); + this.exitTypeDescriptor = "F"; return array[idx]; } else if (arrayComponentType == Byte.TYPE) { byte[] array = (byte[]) ctx; checkAccess(array.length, idx); + this.exitTypeDescriptor = "B"; return array[idx]; } else { Object[] array = (Object[]) ctx; checkAccess(array.length, idx); - return array[idx]; + Object retValue = array[idx]; + this.exitTypeDescriptor = CodeFlow.toDescriptor(arrayComponentType); + return retValue; } } @@ -327,7 +469,7 @@ public class Indexer extends SpelNodeImpl { @SuppressWarnings({"rawtypes", "unchecked"}) - private static class MapIndexingValueRef implements ValueRef { + private class MapIndexingValueRef implements ValueRef { private final TypeConverter typeConverter; @@ -347,6 +489,7 @@ public class Indexer extends SpelNodeImpl { @Override public TypedValue getValue() { Object value = this.map.get(this.key); + exitTypeDescriptor = CodeFlow.toDescriptorFromObject(value); return new TypedValue(value, this.mapEntryTypeDescriptor.getMapValueTypeDescriptor(value)); } @@ -408,7 +551,17 @@ public class Indexer extends SpelNodeImpl { Indexer.this.cachedReadAccessor = accessor; Indexer.this.cachedReadName = this.name; Indexer.this.cachedReadTargetType = targetObjectRuntimeClass; - return accessor.read(this.evaluationContext, this.targetObject, this.name); + if (accessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor) { + ReflectivePropertyAccessor.OptimalPropertyAccessor optimalAccessor = (ReflectivePropertyAccessor.OptimalPropertyAccessor)accessor; + Member member = optimalAccessor.member; + if (member instanceof Field) { + Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(((Field)member).getType()); + } else { + Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(((Method)member).getReturnType()); + } + } + TypedValue value = accessor.read(this.evaluationContext, this.targetObject, this.name); + return value; } } } @@ -490,6 +643,7 @@ public class Indexer extends SpelNodeImpl { growCollectionIfNecessary(); if (this.collection instanceof List) { Object o = ((List) this.collection).get(this.index); + exitTypeDescriptor = CodeFlow.toDescriptorFromObject(o); return new TypedValue(o, this.collectionEntryDescriptor.elementTypeDescriptor(o)); } int pos = 0; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/IntLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/IntLiteral.java index a6481449017..4e2c77f553a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/IntLiteral.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/IntLiteral.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,9 @@ */ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.CodeFlow; /** * Expression language AST node that represents an integer literal. @@ -27,10 +29,10 @@ public class IntLiteral extends Literal { private final TypedValue value; - IntLiteral(String payload, int pos, int value) { super(payload, pos); this.value = new TypedValue(value); + this.exitTypeDescriptor = "I"; } @@ -39,4 +41,25 @@ public class IntLiteral extends Literal { return this.value; } + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + int intValue = ((Integer)this.value.getValue()).intValue(); + if (intValue==-1) { + // Not sure we can get here because -1 is OpMinus + mv.visitInsn(ICONST_M1); + } + else if (intValue >= 0 && intValue < 6) { + mv.visitInsn(ICONST_0+intValue); + } + else { + mv.visitLdcInsn(intValue); + } + codeflow.pushDescriptor(getExitDescriptor()); + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/LongLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/LongLiteral.java index 3404411dbb6..6dd102e868a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/LongLiteral.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/LongLiteral.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.CodeFlow; /** * Expression language AST node that represents a long integer literal. @@ -32,12 +34,23 @@ public class LongLiteral extends Literal { LongLiteral(String payload, int pos, long value) { super(payload, pos); this.value = new TypedValue(value); + this.exitTypeDescriptor = "J"; } - @Override public TypedValue getLiteralValue() { return this.value; } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + mv.visitLdcInsn(this.value.getValue()); + codeflow.pushDescriptor(getExitDescriptor()); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index adcfe7fca87..cfe1b7109a2 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,10 +17,13 @@ package org.springframework.expression.spel.ast; import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; @@ -32,6 +35,8 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.CodeFlow; +import org.springframework.expression.spel.support.ReflectiveMethodExecutor; import org.springframework.expression.spel.support.ReflectiveMethodResolver; /** @@ -77,7 +82,18 @@ public class MethodReference extends SpelNodeImpl { Object value = state.getActiveContextObject().getValue(); TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor(); Object[] arguments = getArguments(state); - return getValueInternal(evaluationContext, value, targetType, arguments); + TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments); + if (cachedExecutor.get() instanceof ReflectiveMethodExecutor) { + ReflectiveMethodExecutor executor = (ReflectiveMethodExecutor) cachedExecutor.get(); + Method method = executor.getMethod(); + exitTypeDescriptor = CodeFlow.toDescriptor(method.getReturnType()); + } + return result; + } + + @Override + public String getExitDescriptor() { + return exitTypeDescriptor; } private TypedValue getValueInternal(EvaluationContext evaluationContext, @@ -161,9 +177,7 @@ public class MethodReference extends SpelNodeImpl { return Collections.unmodifiableList(descriptors); } - private MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object value, - TypeDescriptor target, List argumentTypes) { - + private MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object value, TypeDescriptor target, List argumentTypes) { List methodResolvers = evaluationContext.getMethodResolvers(); if (methodResolvers == null || methodResolvers.size() != 1 || !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) { @@ -255,7 +269,13 @@ public class MethodReference extends SpelNodeImpl { @Override public TypedValue getValue() { - return getValueInternal(this.evaluationContext, this.value, this.targetType, this.arguments); + TypedValue result = MethodReference.this.getValueInternal(this.evaluationContext, this.value, this.targetType, this.arguments); + if (MethodReference.this.cachedExecutor.get() instanceof ReflectiveMethodExecutor) { + ReflectiveMethodExecutor executor = (ReflectiveMethodExecutor) MethodReference.this.cachedExecutor.get(); + Method method = executor.getMethod(); + MethodReference.this.exitTypeDescriptor = CodeFlow.toDescriptor(method.getReturnType()); + } + return result; } @Override @@ -298,4 +318,69 @@ public class MethodReference extends SpelNodeImpl { } } + /** + * A method reference is compilable if it has been resolved to a reflectively accessible method + * and the child nodes (arguments to the method) are also compilable. + */ + @Override + public boolean isCompilable() { + if (this.cachedExecutor == null || !(this.cachedExecutor.get() instanceof ReflectiveMethodExecutor)) { + return false; + } + for (SpelNodeImpl child: children) { + if (!child.isCompilable()) { + return false; + } + } + ReflectiveMethodExecutor executor = (ReflectiveMethodExecutor) this.cachedExecutor.get(); + Method method = executor.getMethod(); + if (!Modifier.isPublic(method.getModifiers()) || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + return false; + } + if (method.isVarArgs()) { + return false; + } + if (executor.didArgumentConversionOccur()) { + return false; + } + return true; + } + + @Override + public void generateCode(MethodVisitor mv,CodeFlow codeflow) { + ReflectiveMethodExecutor executor = (ReflectiveMethodExecutor) this.cachedExecutor.get(); + Method method = executor.getMethod(); + boolean isStaticMethod = Modifier.isStatic(method.getModifiers()); + String descriptor = codeflow.lastDescriptor(); + + if (descriptor == null && !isStaticMethod) { + codeflow.loadTarget(mv); + } + + boolean itf = method.getDeclaringClass().isInterface(); + String methodDeclaringClassSlashedDescriptor = method.getDeclaringClass().getName().replace('.','/'); + if (!isStaticMethod) { + if (descriptor == null || !descriptor.equals(method.getDeclaringClass())) { + mv.visitTypeInsn(CHECKCAST, method.getDeclaringClass().getName().replace('.','/')); + } + } + String[] paramDescriptors = CodeFlow.toParamDescriptors(method); + for (int c=0;c1) { + if (!getRightOperand().isCompilable()) { + return false; + } + } + return this.exitTypeDescriptor!=null; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + getLeftOperand().generateCode(mv, codeflow); + String leftdesc = getLeftOperand().getExitDescriptor(); + if (!CodeFlow.isPrimitive(leftdesc)) { + CodeFlow.insertUnboxInsns(mv, this.exitTypeDescriptor.charAt(0), false); + } + if (this.children.length > 1) { + getRightOperand().generateCode(mv, codeflow); + String rightdesc = getRightOperand().getExitDescriptor(); + if (!CodeFlow.isPrimitive(rightdesc)) { + CodeFlow.insertUnboxInsns(mv, this.exitTypeDescriptor.charAt(0), false); + } + switch (this.exitTypeDescriptor.charAt(0)) { + case 'I': + mv.visitInsn(IDIV); + break; + case 'J': + mv.visitInsn(LDIV); + break; + case 'F': + mv.visitInsn(FDIV); + break; + case 'D': + mv.visitInsn(DDIV); + break; + default: + throw new IllegalStateException("Unrecognized exit descriptor: '"+this.exitTypeDescriptor+"'"); + } + } + codeflow.pushDescriptor(this.exitTypeDescriptor); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java index 06366e5e892..a3aa5dcc6f5 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpEQ.java @@ -16,8 +16,11 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; /** @@ -30,6 +33,7 @@ public class OpEQ extends Operator { public OpEQ(int pos, SpelNodeImpl... operands) { super("==", pos, operands); + this.exitTypeDescriptor = "Z"; } @Override @@ -38,5 +42,100 @@ public class OpEQ extends Operator { Object right = getRightOperand().getValueInternal(state).getValue(); return BooleanTypedValue.forValue(equalityCheck(state, left, right)); } + + // This check is different to the one in the other numeric operators (OpLt/etc) + // because it allows for simple object comparison + @Override + public boolean isCompilable() { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right= getRightOperand(); + if (!left.isCompilable() || !right.isCompilable()) { + return false; + } + String leftdesc = left.getExitDescriptor(); + String rightdesc = right.getExitDescriptor(); + if ((CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(leftdesc) || + CodeFlow.isPrimitiveOrUnboxableSupportedNumber(rightdesc))) { + if (!CodeFlow.areBoxingCompatible(leftdesc, rightdesc)) { + return false; + } + } + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + String leftDesc = getLeftOperand().getExitDescriptor(); + String rightDesc = getRightOperand().getExitDescriptor(); + Label elseTarget = new Label(); + Label endOfIf = new Label(); + boolean leftPrim = CodeFlow.isPrimitive(leftDesc); + boolean rightPrim = CodeFlow.isPrimitive(rightDesc); + + if ((CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(leftDesc) || + CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(rightDesc)) && + CodeFlow.areBoxingCompatible(leftDesc,rightDesc)) { + char targetType = CodeFlow.toPrimitiveTargetDesc(leftDesc); + + getLeftOperand().generateCode(mv, codeflow); + if (!leftPrim) { + CodeFlow.insertUnboxInsns(mv, targetType, false); + } + + getRightOperand().generateCode(mv, codeflow); + if (!rightPrim) { + CodeFlow.insertUnboxInsns(mv, targetType, false); + } + // assert: SpelCompiler.boxingCompatible(leftDesc, rightDesc) + if (targetType=='D') { + mv.visitInsn(DCMPL); + mv.visitJumpInsn(IFNE, elseTarget); + } + else if (targetType=='F') { + mv.visitInsn(FCMPL); + mv.visitJumpInsn(IFNE, elseTarget); + } + else if (targetType=='J') { + mv.visitInsn(LCMP); + mv.visitJumpInsn(IFNE, elseTarget); + } + else if (targetType=='I' || targetType=='Z') { + mv.visitJumpInsn(IF_ICMPNE, elseTarget); + } + else { + throw new IllegalStateException("Unexpected descriptor "+leftDesc); + } + } else { + getLeftOperand().generateCode(mv, codeflow); + getRightOperand().generateCode(mv, codeflow); + Label leftNotNull = new Label(); + mv.visitInsn(DUP_X1); // Dup right on the top of the stack + mv.visitJumpInsn(IFNONNULL,leftNotNull); + // Right is null! + mv.visitInsn(SWAP); + mv.visitInsn(POP); // remove it + Label rightNotNull = new Label(); + mv.visitJumpInsn(IFNONNULL, rightNotNull); + // Left is null too + mv.visitInsn(ICONST_1); + mv.visitJumpInsn(GOTO, endOfIf); + mv.visitLabel(rightNotNull); + mv.visitInsn(ICONST_0); + mv.visitJumpInsn(GOTO,endOfIf); + + + mv.visitLabel(leftNotNull); + mv.visitMethodInsn(INVOKEVIRTUAL,"java/lang/Object","equals","(Ljava/lang/Object;)Z",false); + mv.visitLabel(endOfIf); + codeflow.pushDescriptor("Z"); + return; + } + mv.visitInsn(ICONST_1); + mv.visitJumpInsn(GOTO,endOfIf); + mv.visitLabel(elseTarget); + mv.visitInsn(ICONST_0); + mv.visitLabel(endOfIf); + codeflow.pushDescriptor("Z"); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGE.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGE.java index dbd0b21510b..48b43df7188 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGE.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGE.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,10 +15,11 @@ */ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import java.math.BigDecimal; - import org.springframework.expression.EvaluationException; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; import org.springframework.util.NumberUtils; @@ -34,6 +35,7 @@ public class OpGE extends Operator { public OpGE(int pos, SpelNodeImpl... operands) { super(">=", pos, operands); + this.exitTypeDescriptor="Z"; } @@ -66,5 +68,15 @@ public class OpGE extends Operator { } return BooleanTypedValue.forValue(state.getTypeComparator().compare(left, right) >= 0); } + + @Override + public boolean isCompilable() { + return isCompilableOperatorUsingNumerics(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + generateComparisonCode(mv, codeflow, IFLT, IF_ICMPLT); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGT.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGT.java index 2be6eb70900..2780ba96e88 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGT.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpGT.java @@ -16,10 +16,11 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import java.math.BigDecimal; - import org.springframework.expression.EvaluationException; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; import org.springframework.util.NumberUtils; @@ -35,6 +36,7 @@ public class OpGT extends Operator { public OpGT(int pos, SpelNodeImpl... operands) { super(">", pos, operands); + this.exitTypeDescriptor = "Z"; } @Override @@ -75,4 +77,13 @@ public class OpGT extends Operator { return BooleanTypedValue.forValue(state.getTypeComparator().compare(left, right) > 0); } + @Override + public boolean isCompilable() { + return isCompilableOperatorUsingNumerics(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + generateComparisonCode(mv, codeflow, IFLE, IF_ICMPLE); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLE.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLE.java index d7991f816aa..98fd4ecbbec 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLE.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLE.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,11 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import java.math.BigDecimal; - import org.springframework.expression.EvaluationException; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; import org.springframework.util.NumberUtils; @@ -35,6 +36,7 @@ public class OpLE extends Operator { public OpLE(int pos, SpelNodeImpl... operands) { super("<=", pos, operands); + this.exitTypeDescriptor="Z"; } @@ -70,5 +72,15 @@ public class OpLE extends Operator { return BooleanTypedValue.forValue(state.getTypeComparator().compare(left, right) <= 0); } + + @Override + public boolean isCompilable() { + return isCompilableOperatorUsingNumerics(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + generateComparisonCode(mv, codeflow, IFGT, IF_ICMPGT); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLT.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLT.java index 20a53c07353..ee36f1320fe 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLT.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpLT.java @@ -16,10 +16,11 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import java.math.BigDecimal; - import org.springframework.expression.EvaluationException; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; import org.springframework.util.NumberUtils; @@ -35,6 +36,7 @@ public class OpLT extends Operator { public OpLT(int pos, SpelNodeImpl... operands) { super("<", pos, operands); + this.exitTypeDescriptor = "Z"; } @Override @@ -74,5 +76,15 @@ public class OpLT extends Operator { return BooleanTypedValue.forValue(state.getTypeComparator().compare(left, right) < 0); } + + @Override + public boolean isCompilable() { + return isCompilableOperatorUsingNumerics(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + generateComparisonCode(mv, codeflow, IFGE, IF_ICMPGE); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java index b283b7278de..acbc5deca0c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMinus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; + import java.math.BigDecimal; import org.springframework.expression.EvaluationException; import org.springframework.expression.Operation; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.util.NumberUtils; /** @@ -67,17 +70,20 @@ public class OpMinus extends Operator { } if (operand instanceof Double) { + this.exitTypeDescriptor = "D"; return new TypedValue(0 - n.doubleValue()); } if (operand instanceof Float) { + this.exitTypeDescriptor = "F"; return new TypedValue(0 - n.floatValue()); } if (operand instanceof Long) { + this.exitTypeDescriptor = "J"; return new TypedValue(0 - n.longValue()); } - + this.exitTypeDescriptor = "I"; return new TypedValue(0 - n.intValue()); } @@ -96,19 +102,28 @@ public class OpMinus extends Operator { BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); return new TypedValue(leftBigDecimal.subtract(rightBigDecimal)); } - + if (leftNumber instanceof Double || rightNumber instanceof Double) { + if (leftNumber instanceof Double && rightNumber instanceof Double) { + this.exitTypeDescriptor = "D"; + } return new TypedValue(leftNumber.doubleValue() - rightNumber.doubleValue()); } if (leftNumber instanceof Float || rightNumber instanceof Float) { + if (leftNumber instanceof Float && rightNumber instanceof Float) { + this.exitTypeDescriptor = "F"; + } return new TypedValue(leftNumber.floatValue() - rightNumber.floatValue()); } if (leftNumber instanceof Long || rightNumber instanceof Long) { + if (leftNumber instanceof Long && rightNumber instanceof Long) { + this.exitTypeDescriptor = "J"; + } return new TypedValue(leftNumber.longValue() - rightNumber.longValue()); } - + this.exitTypeDescriptor = "I"; return new TypedValue(leftNumber.intValue() - rightNumber.intValue()); } else if (left instanceof String && right instanceof Integer @@ -130,10 +145,74 @@ public class OpMinus extends Operator { } return super.toStringAST(); } + @Override public SpelNodeImpl getRightOperand() { if (this.children.length<2) {return null;} return this.children[1]; } + + @Override + public boolean isCompilable() { + if (!getLeftOperand().isCompilable()) { + return false; + } + if (this.children.length>1) { + if (!getRightOperand().isCompilable()) { + return false; + } + } + return this.exitTypeDescriptor!=null; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + getLeftOperand().generateCode(mv, codeflow); + String leftdesc = getLeftOperand().getExitDescriptor(); + if (!CodeFlow.isPrimitive(leftdesc)) { + CodeFlow.insertUnboxInsns(mv, this.exitTypeDescriptor.charAt(0), false); + } + if (this.children.length>1) { + getRightOperand().generateCode(mv, codeflow); + String rightdesc = getRightOperand().getExitDescriptor(); + if (!CodeFlow.isPrimitive(rightdesc)) { + CodeFlow.insertUnboxInsns(mv, this.exitTypeDescriptor.charAt(0), false); + } + switch (this.exitTypeDescriptor.charAt(0)) { + case 'I': + mv.visitInsn(ISUB); + break; + case 'J': + mv.visitInsn(LSUB); + break; + case 'F': + mv.visitInsn(FSUB); + break; + case 'D': + mv.visitInsn(DSUB); + break; + default: + throw new IllegalStateException("Unrecognized exit descriptor: '"+this.exitTypeDescriptor+"'"); + } + } else { + switch (this.exitTypeDescriptor.charAt(0)) { + case 'I': + mv.visitInsn(INEG); + break; + case 'J': + mv.visitInsn(LNEG); + break; + case 'F': + mv.visitInsn(FNEG); + break; + case 'D': + mv.visitInsn(DNEG); + break; + default: + throw new IllegalStateException("Unrecognized exit descriptor: '"+this.exitTypeDescriptor+"'"); + } + } + codeflow.pushDescriptor(this.exitTypeDescriptor); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java index 870ceb1fdf8..ff0c967d2df 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpMultiply.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; + import java.math.BigDecimal; import org.springframework.expression.EvaluationException; import org.springframework.expression.Operation; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.util.NumberUtils; /** @@ -81,17 +84,29 @@ public class OpMultiply extends Operator { } if (leftNumber instanceof Double || rightNumber instanceof Double) { - return new TypedValue(leftNumber.doubleValue() * rightNumber.doubleValue()); + if (leftNumber instanceof Double && rightNumber instanceof Double) { + this.exitTypeDescriptor = "D"; + } + return new TypedValue(leftNumber.doubleValue() + * rightNumber.doubleValue()); } if (leftNumber instanceof Float || rightNumber instanceof Float) { + if (leftNumber instanceof Float && rightNumber instanceof Float) { + this.exitTypeDescriptor = "F"; + } return new TypedValue(leftNumber.floatValue() * rightNumber.floatValue()); } if (leftNumber instanceof Long || rightNumber instanceof Long) { + if (leftNumber instanceof Long && rightNumber instanceof Long) { + this.exitTypeDescriptor = "J"; + } return new TypedValue(leftNumber.longValue() * rightNumber.longValue()); } - + if (leftNumber instanceof Integer && rightNumber instanceof Integer) { + this.exitTypeDescriptor = "I"; + } return new TypedValue(leftNumber.intValue() * rightNumber.intValue()); } else if (leftOperand instanceof String && rightOperand instanceof Integer) { @@ -105,5 +120,51 @@ public class OpMultiply extends Operator { return state.operate(Operation.MULTIPLY, leftOperand, rightOperand); } + + @Override + public boolean isCompilable() { + if (!getLeftOperand().isCompilable()) { + return false; + } + if (this.children.length>1) { + if (!getRightOperand().isCompilable()) { + return false; + } + } + return this.exitTypeDescriptor!=null; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + getLeftOperand().generateCode(mv, codeflow); + String leftdesc = getLeftOperand().getExitDescriptor(); + if (!CodeFlow.isPrimitive(leftdesc)) { + CodeFlow.insertUnboxInsns(mv, this.exitTypeDescriptor.charAt(0), false); + } + if (this.children.length>1) { + getRightOperand().generateCode(mv, codeflow); + String rightdesc = getRightOperand().getExitDescriptor(); + if (!CodeFlow.isPrimitive(rightdesc)) { + CodeFlow.insertUnboxInsns(mv, this.exitTypeDescriptor.charAt(0), false); + } + switch (this.exitTypeDescriptor.charAt(0)) { + case 'I': + mv.visitInsn(IMUL); + break; + case 'J': + mv.visitInsn(LMUL); + break; + case 'F': + mv.visitInsn(FMUL); + break; + case 'D': + mv.visitInsn(DMUL); + break; + default: + throw new IllegalStateException("Unrecognized exit descriptor: '"+this.exitTypeDescriptor+"'"); + } + } + codeflow.pushDescriptor(this.exitTypeDescriptor); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java index f4f5e7a773e..7af16b550fc 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpNE.java @@ -16,8 +16,11 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; /** @@ -30,6 +33,7 @@ public class OpNE extends Operator { public OpNE(int pos, SpelNodeImpl... operands) { super("!=", pos, operands); + this.exitTypeDescriptor = "Z"; } @Override @@ -39,4 +43,79 @@ public class OpNE extends Operator { return BooleanTypedValue.forValue(!equalityCheck(state, left, right)); } + // This check is different to the one in the other numeric operators (OpLt/etc) + // because we allow simple object comparison + @Override + public boolean isCompilable() { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right= getRightOperand(); + if (!left.isCompilable() || !right.isCompilable()) { + return false; + } + String leftdesc = left.getExitDescriptor(); + String rightdesc = right.getExitDescriptor(); + if ((CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(leftdesc) || + CodeFlow.isPrimitiveOrUnboxableSupportedNumber(rightdesc))) { + if (!CodeFlow.areBoxingCompatible(leftdesc, rightdesc)) { + return false; + } + } + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + String leftDesc = getLeftOperand().getExitDescriptor(); + String rightDesc = getRightOperand().getExitDescriptor(); + Label elseTarget = new Label(); + Label endOfIf = new Label(); + boolean leftPrim = CodeFlow.isPrimitive(leftDesc); + boolean rightPrim = CodeFlow.isPrimitive(rightDesc); + + if ((CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(leftDesc) || + CodeFlow.isPrimitiveOrUnboxableSupportedNumberOrBoolean(rightDesc)) && + CodeFlow.areBoxingCompatible(leftDesc,rightDesc)) { + char targetType = CodeFlow.toPrimitiveTargetDesc(leftDesc); + + getLeftOperand().generateCode(mv, codeflow); + if (!leftPrim) { + CodeFlow.insertUnboxInsns(mv, targetType, false); + } + + getRightOperand().generateCode(mv, codeflow); + if (!rightPrim) { + CodeFlow.insertUnboxInsns(mv, targetType, false); + } + // assert: SpelCompiler.boxingCompatible(leftDesc, rightDesc) + if (targetType == 'D') { + mv.visitInsn(DCMPL); + mv.visitJumpInsn(IFEQ, elseTarget); + } + else if (targetType == 'F') { + mv.visitInsn(FCMPL); + mv.visitJumpInsn(IFEQ, elseTarget); + } + else if (targetType == 'J') { + mv.visitInsn(LCMP); + mv.visitJumpInsn(IFEQ, elseTarget); + } + else if (targetType == 'I' || targetType == 'Z') { + mv.visitJumpInsn(IF_ICMPEQ, elseTarget); + } + else { + throw new IllegalStateException("Unexpected descriptor "+leftDesc); + } + } else { + getLeftOperand().generateCode(mv, codeflow); + getRightOperand().generateCode(mv, codeflow); + mv.visitJumpInsn(IF_ACMPEQ, elseTarget); + } + mv.visitInsn(ICONST_1); + mv.visitJumpInsn(GOTO,endOfIf); + mv.visitLabel(elseTarget); + mv.visitInsn(ICONST_0); + mv.visitLabel(endOfIf); + codeflow.pushDescriptor("Z"); + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java index ce21a1cb9ab..b7213c374fb 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,13 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; /** @@ -34,6 +37,7 @@ public class OpOr extends Operator { public OpOr(int pos, SpelNodeImpl... operands) { super("or", pos, operands); + this.exitTypeDescriptor = "Z"; } @@ -64,4 +68,37 @@ public class OpOr extends Operator { } } + @Override + public boolean isCompilable() { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right= getRightOperand(); + if (!left.isCompilable() || !right.isCompilable()) { + return false; + } + return + CodeFlow.isBooleanCompatible(left.getExitDescriptor()) && + CodeFlow.isBooleanCompatible(right.getExitDescriptor()); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + // pseudo: if (leftOperandValue) { result=true; } else { result=rightOperandValue; } + Label elseTarget = new Label(); + Label endOfIf = new Label(); + codeflow.enterCompilationScope(); + getLeftOperand().generateCode(mv, codeflow); + codeflow.unboxBooleanIfNecessary(mv); + codeflow.exitCompilationScope(); + mv.visitJumpInsn(IFEQ, elseTarget); + mv.visitLdcInsn(1); // TRUE + mv.visitJumpInsn(GOTO,endOfIf); + mv.visitLabel(elseTarget); + codeflow.enterCompilationScope(); + getRightOperand().generateCode(mv, codeflow); + codeflow.unboxBooleanIfNecessary(mv); + codeflow.exitCompilationScope(); + mv.visitLabel(endOfIf); + codeflow.pushDescriptor(getExitDescriptor()); + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java index 5a38866cd22..74a8ad93da1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; + import java.math.BigDecimal; import org.springframework.core.convert.TypeDescriptor; @@ -24,6 +26,7 @@ import org.springframework.expression.Operation; import org.springframework.expression.TypeConverter; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; @@ -63,11 +66,16 @@ public class OpPlus extends Operator { Object operandOne = leftOp.getValueInternal(state).getValue(); if (operandOne instanceof Number) { if (operandOne instanceof Double || operandOne instanceof Long || operandOne instanceof BigDecimal) { + if (operandOne instanceof Double || operandOne instanceof Long) { + this.exitTypeDescriptor = (operandOne instanceof Double)?"D":"J"; + } return new TypedValue(operandOne); } if (operandOne instanceof Float) { + this.exitTypeDescriptor = "F"; return new TypedValue(((Number) operandOne).floatValue()); } + this.exitTypeDescriptor = "I"; return new TypedValue(((Number) operandOne).intValue()); } return state.operate(Operation.ADD, operandOne, null); @@ -88,20 +96,27 @@ public class OpPlus extends Operator { BigDecimal rightBigDecimal = NumberUtils.convertNumberToTargetClass(rightNumber, BigDecimal.class); return new TypedValue(leftBigDecimal.add(rightBigDecimal)); } - if (leftNumber instanceof Double || rightNumber instanceof Double) { + if (leftNumber instanceof Double && rightNumber instanceof Double) { + this.exitTypeDescriptor = "D"; + } return new TypedValue(leftNumber.doubleValue() + rightNumber.doubleValue()); } - if (leftNumber instanceof Float || rightNumber instanceof Float) { + if (leftNumber instanceof Float && rightNumber instanceof Float) { + this.exitTypeDescriptor = "F"; + } return new TypedValue(leftNumber.floatValue() + rightNumber.floatValue()); } - if (leftNumber instanceof Long || rightNumber instanceof Long) { + if (leftNumber instanceof Long && rightNumber instanceof Long) { + this.exitTypeDescriptor = "J"; + } return new TypedValue(leftNumber.longValue() + rightNumber.longValue()); } // TODO what about overflow? + this.exitTypeDescriptor = "I"; return new TypedValue(leftNumber.intValue() + rightNumber.intValue()); } @@ -164,4 +179,50 @@ public class OpPlus extends Operator { return String.valueOf(value.getValue()); } + @Override + public boolean isCompilable() { + if (!getLeftOperand().isCompilable()) { + return false; + } + if (this.children.length>1) { + if (!getRightOperand().isCompilable()) { + return false; + } + } + return this.exitTypeDescriptor!=null; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + getLeftOperand().generateCode(mv, codeflow); + String leftdesc = getLeftOperand().getExitDescriptor(); + if (!CodeFlow.isPrimitive(leftdesc)) { + CodeFlow.insertUnboxInsns(mv, this.exitTypeDescriptor.charAt(0), false); + } + if (this.children.length>1) { + getRightOperand().generateCode(mv, codeflow); + String rightdesc = getRightOperand().getExitDescriptor(); + if (!CodeFlow.isPrimitive(rightdesc)) { + CodeFlow.insertUnboxInsns(mv, this.exitTypeDescriptor.charAt(0), false); + } + switch (this.exitTypeDescriptor.charAt(0)) { + case 'I': + mv.visitInsn(IADD); + break; + case 'J': + mv.visitInsn(LADD); + break; + case 'F': + mv.visitInsn(FADD); + break; + case 'D': + mv.visitInsn(DADD); + break; + default: + throw new IllegalStateException("Unrecognized exit descriptor: '"+this.exitTypeDescriptor+"'"); + } + } + codeflow.pushDescriptor(this.exitTypeDescriptor); + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java index d797d2115a4..0e7bd63f67a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java @@ -16,6 +16,10 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.spel.standard.CodeFlow; + import java.math.BigDecimal; import org.springframework.expression.spel.ExpressionState; @@ -72,6 +76,74 @@ public abstract class Operator extends SpelNodeImpl { return sb.toString(); } + protected boolean isCompilableOperatorUsingNumerics() { + SpelNodeImpl left = getLeftOperand(); + SpelNodeImpl right= getRightOperand(); + if (!left.isCompilable() || !right.isCompilable()) { + return false; + } + // Supported operand types for equals (at the moment) + String leftDesc = left.getExitDescriptor(); + String rightDesc= right.getExitDescriptor(); + if (CodeFlow.isPrimitiveOrUnboxableSupportedNumber(leftDesc) && CodeFlow.isPrimitiveOrUnboxableSupportedNumber(rightDesc)) { + if (CodeFlow.areBoxingCompatible(leftDesc, rightDesc)) { + return true; + } + } + return false; + } + + /** + * Numeric comparison operators share very similar generated code, only differing in + * two comparison instructions. + */ + protected void generateComparisonCode(MethodVisitor mv, CodeFlow codeflow, int compareInstruction1, + int compareInstruction2) { + String leftDesc = getLeftOperand().getExitDescriptor(); + String rightDesc = getRightOperand().getExitDescriptor(); + + boolean unboxLeft = !CodeFlow.isPrimitive(leftDesc); + boolean unboxRight = !CodeFlow.isPrimitive(rightDesc); + char targetType = CodeFlow.toPrimitiveTargetDesc(leftDesc); + + getLeftOperand().generateCode(mv, codeflow); + if (unboxLeft) { + CodeFlow.insertUnboxInsns(mv, targetType, false); + } + + getRightOperand().generateCode(mv, codeflow); + if (unboxRight) { + CodeFlow.insertUnboxInsns(mv, targetType, false); + } + // assert: SpelCompiler.boxingCompatible(leftDesc, rightDesc) + Label elseTarget = new Label(); + Label endOfIf = new Label(); + if (targetType=='D') { + mv.visitInsn(DCMPG); + mv.visitJumpInsn(compareInstruction1, elseTarget); + } + else if (targetType=='F') { + mv.visitInsn(FCMPG); + mv.visitJumpInsn(compareInstruction1, elseTarget); + } + else if (targetType=='J') { + mv.visitInsn(LCMP); + mv.visitJumpInsn(compareInstruction1, elseTarget); + } + else if (targetType=='I') { + mv.visitJumpInsn(compareInstruction2, elseTarget); + } + else { + throw new IllegalStateException("Unexpected descriptor "+leftDesc); + } + // Other numbers are not yet supported (isCompilable will not have returned true) + mv.visitInsn(ICONST_1); + mv.visitJumpInsn(GOTO,endOfIf); + mv.visitLabel(elseTarget); + mv.visitInsn(ICONST_0); + mv.visitLabel(endOfIf); + codeflow.pushDescriptor("Z"); + } protected boolean equalityCheck(ExpressionState state, Object left, Object right) { if (left instanceof Number && right instanceof Number) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java index 2f50e86c828..ed9c403440b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,17 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Type; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; + /** * The operator 'instanceof' checks if an object is of the class specified in the right * hand operand, in the same way that {@code instanceof} does in Java. @@ -32,6 +36,8 @@ import org.springframework.expression.spel.support.BooleanTypedValue; */ public class OperatorInstanceof extends Operator { + private Class type; + public OperatorInstanceof(int pos, SpelNodeImpl... operands) { super("instanceof", pos, operands); } @@ -51,16 +57,32 @@ public class OperatorInstanceof extends Operator { TypedValue right = getRightOperand().getValueInternal(state); Object leftValue = left.getValue(); Object rightValue = right.getValue(); - if (leftValue == null) { - return BooleanTypedValue.FALSE; // null is not an instanceof anything - } + BooleanTypedValue result = null; if (rightValue == null || !(rightValue instanceof Class)) { throw new SpelEvaluationException(getRightOperand().getStartPosition(), SpelMessage.INSTANCEOF_OPERATOR_NEEDS_CLASS_OPERAND, (rightValue == null ? "null" : rightValue.getClass().getName())); } Class rightClass = (Class) rightValue; - return BooleanTypedValue.forValue(rightClass.isAssignableFrom(leftValue.getClass())); + if (leftValue == null) { + result = BooleanTypedValue.FALSE; // null is not an instanceof anything + } else { + result = BooleanTypedValue.forValue(rightClass.isAssignableFrom(leftValue.getClass())); + } + this.type = rightClass; + this.exitTypeDescriptor = "Z"; + return result; } + @Override + public boolean isCompilable() { + return this.exitTypeDescriptor != null && getLeftOperand().isCompilable(); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + getLeftOperand().generateCode(mv, codeflow); + mv.visitTypeInsn(INSTANCEOF,Type.getInternalName(this.type)); + codeflow.pushDescriptor(getExitDescriptor()); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorNot.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorNot.java index 3e5be7217ad..f266b72cfc8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorNot.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorNot.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,13 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.BooleanTypedValue; /** @@ -34,6 +37,7 @@ public class OperatorNot extends SpelNodeImpl { // Not is a unary operator so do public OperatorNot(int pos, SpelNodeImpl operand) { super(pos, operand); + this.exitTypeDescriptor = "Z"; } @@ -58,5 +62,26 @@ public class OperatorNot extends SpelNodeImpl { // Not is a unary operator so do sb.append("!").append(getChild(0).toStringAST()); return sb.toString(); } + + @Override + public boolean isCompilable() { + SpelNodeImpl child = this.children[0]; + return child.isCompilable() && CodeFlow.isBooleanCompatible(child.getExitDescriptor()); + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + this.children[0].generateCode(mv, codeflow); + codeflow.unboxBooleanIfNecessary(mv); + Label elseTarget = new Label(); + Label endOfIf = new Label(); + mv.visitJumpInsn(IFNE,elseTarget); + mv.visitInsn(ICONST_1); // TRUE + mv.visitJumpInsn(GOTO,endOfIf); + mv.visitLabel(elseTarget); + mv.visitInsn(ICONST_0); // FALSE + mv.visitLabel(endOfIf); + codeflow.pushDescriptor(getExitDescriptor()); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index 478b7172940..097b93f03aa 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -21,8 +21,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; +import org.springframework.expression.CompilablePropertyAccessor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.PropertyAccessor; @@ -30,6 +32,7 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.ReflectivePropertyAccessor; /** @@ -75,8 +78,17 @@ public class PropertyOrFieldReference extends SpelNodeImpl { @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { - return getValueInternal(state.getActiveContextObject(), state.getEvaluationContext(), - state.getConfiguration().isAutoGrowNullReferences()); + TypedValue tv = getValueInternal(state.getActiveContextObject(), state.getEvaluationContext(), state.getConfiguration().isAutoGrowNullReferences()); + if (cachedReadAccessor instanceof CompilablePropertyAccessor) { + CompilablePropertyAccessor accessor = (CompilablePropertyAccessor)cachedReadAccessor; + exitTypeDescriptor = CodeFlow.toDescriptor(accessor.getPropertyType()); + } + return tv; + } + + @Override + public String getExitDescriptor() { + return exitTypeDescriptor; } private TypedValue getValueInternal(TypedValue contextObject, EvaluationContext eContext, @@ -316,6 +328,23 @@ public class PropertyOrFieldReference extends SpelNodeImpl { resolvers.addAll(generalAccessors); return resolvers; } + + @Override + public boolean isCompilable() { + if (this.cachedReadAccessor == null) { + return false; + } + if (this.cachedReadAccessor instanceof CompilablePropertyAccessor) { + return ((CompilablePropertyAccessor)this.cachedReadAccessor).isCompilable(); + } + return false; + } + + @Override + public void generateCode(MethodVisitor mv,CodeFlow codeflow) { + ((CompilablePropertyAccessor)this.cachedReadAccessor).generateCode(this, mv, codeflow); + codeflow.pushDescriptor(exitTypeDescriptor); + } private static class AccessorLValue implements ValueRef { @@ -338,7 +367,12 @@ public class PropertyOrFieldReference extends SpelNodeImpl { @Override public TypedValue getValue() { - return this.ref.getValueInternal(this.contextObject, this.eContext, this.autoGrowNullReferences); + TypedValue value = this.ref.getValueInternal(this.contextObject, this.eContext, this.autoGrowNullReferences); + if (ref.cachedReadAccessor instanceof CompilablePropertyAccessor) { + CompilablePropertyAccessor accessor = (CompilablePropertyAccessor)this.ref.cachedReadAccessor; + this.ref.exitTypeDescriptor = CodeFlow.toDescriptor(accessor.getPropertyType()); + } + return value; } @Override diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/RealLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/RealLiteral.java index 2a7f3d8744a..6a03106ef0b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/RealLiteral.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/RealLiteral.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,9 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.CodeFlow; /** * Expression language AST node that represents a real literal. @@ -32,6 +34,7 @@ public class RealLiteral extends Literal { public RealLiteral(String payload, int pos, double value) { super(payload, pos); this.value = new TypedValue(value); + this.exitTypeDescriptor = "D"; } @@ -40,4 +43,16 @@ public class RealLiteral extends Literal { return this.value; } + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + mv.visitLdcInsn(this.value.getValue()); + codeflow.pushDescriptor(getExitDescriptor()); + } + + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java index 5b0e2138c16..798923a2b86 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java @@ -16,6 +16,8 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.common.ExpressionUtils; @@ -23,6 +25,7 @@ import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; @@ -33,7 +36,7 @@ import org.springframework.util.Assert; * @author Andy Clement * @since 3.0 */ -public abstract class SpelNodeImpl implements SpelNode { +public abstract class SpelNodeImpl implements SpelNode, Opcodes { private static SpelNodeImpl[] NO_CHILDREN = new SpelNodeImpl[0]; @@ -44,6 +47,16 @@ public abstract class SpelNodeImpl implements SpelNode { private SpelNodeImpl parent; + /** + * Indicates the type descriptor for the result of this expression node. This is + * set as soon as it is known. For a literal node it is known immediately. For + * a property access or method invocation it is known after one evaluation of + * that node. + * The descriptor is like the bytecode form but is slightly easier to work with. It + * does not include the trailing semicolon (for non array reference types). Some examples: + * Ljava/lang/String, I, [I + */ + protected String exitTypeDescriptor; public SpelNodeImpl(int pos, SpelNodeImpl... operands) { this.pos = pos; @@ -167,6 +180,33 @@ public abstract class SpelNodeImpl implements SpelNode { throw new SpelEvaluationException(this.pos, SpelMessage.NOT_ASSIGNABLE, toStringAST()); } + /** + * Check whether a node can be compiled to bytecode. The reasoning in each node may + * be different but will typically involve checking whether the exit type descriptor + * of the node is known and any relevant child nodes are compilable. + * + * @return true if this node can be compiled to bytecode + */ + public boolean isCompilable() { + return false; + } + + /** + * Generate the bytecode for this node into the supplied visitor. Context info about + * the current expression being compiled is available in the codeflow object. For + * example it will include information about the type of the object currently + * on the stack. + * + * @param mv the ASM MethodVisitor into which code should be generated + * @param codeflow a context object with info about what is on the stack + */ + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + throw new IllegalStateException(this.getClass().getName()+" has no generateCode(..) method"); + } + + public String getExitDescriptor() { + return this.exitTypeDescriptor; + } public abstract TypedValue getValueInternal(ExpressionState expressionState) throws EvaluationException; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java index 3755a862847..46f46da3974 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/StringLiteral.java @@ -16,7 +16,9 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.standard.CodeFlow; /** * Expression language AST node that represents a string literal. @@ -32,9 +34,9 @@ public class StringLiteral extends Literal { public StringLiteral(String payload, int pos, String value) { super(payload,pos); - // TODO should these have been skipped being created by the parser rules? or not? value = value.substring(1, value.length() - 1); this.value = new TypedValue(value.replaceAll("''", "'").replaceAll("\"\"", "\"")); + this.exitTypeDescriptor = "Ljava/lang/String"; } @@ -47,5 +49,16 @@ public class StringLiteral extends Literal { public String toString() { return "'" + getLiteralValue().getValue() + "'"; } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + mv.visitLdcInsn(this.value.getValue()); + codeflow.pushDescriptor(getExitDescriptor()); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java index c637adfb860..eac2af4f9e3 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,14 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.Label; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; +import org.springframework.expression.spel.standard.CodeFlow; /** * Represents a ternary expression, for example: "someCheck()?true:false". @@ -50,18 +53,82 @@ public class Ternary extends SpelNodeImpl { throw new SpelEvaluationException(getChild(0).getStartPosition(), SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean"); } + TypedValue result = null; if (value.booleanValue()) { - return this.children[1].getValueInternal(state); + result = this.children[1].getValueInternal(state); } else { - return this.children[2].getValueInternal(state); + result = this.children[2].getValueInternal(state); } + computeExitTypeDescriptor(); + return result; } - + @Override public String toStringAST() { return new StringBuilder().append(getChild(0).toStringAST()).append(" ? ").append(getChild(1).toStringAST()) .append(" : ").append(getChild(2).toStringAST()).toString(); } + private void computeExitTypeDescriptor() { + if (exitTypeDescriptor == null && this.children[1].getExitDescriptor()!=null && this.children[2].getExitDescriptor()!=null) { + String leftDescriptor = this.children[1].exitTypeDescriptor; + String rightDescriptor = this.children[2].exitTypeDescriptor; + if (leftDescriptor.equals(rightDescriptor)) { + this.exitTypeDescriptor = leftDescriptor; + } + else if (leftDescriptor.equals("Ljava/lang/Object") && !CodeFlow.isPrimitive(rightDescriptor)) { + this.exitTypeDescriptor = rightDescriptor; + } + else if (rightDescriptor.equals("Ljava/lang/Object") && !CodeFlow.isPrimitive(leftDescriptor)) { + this.exitTypeDescriptor = leftDescriptor; + } + else { + // Use the easiest to compute common super type + this.exitTypeDescriptor = "Ljava/lang/Object"; + } + } + } + + @Override + public boolean isCompilable() { + SpelNodeImpl condition = this.children[0]; + SpelNodeImpl left = this.children[1]; + SpelNodeImpl right = this.children[2]; + if (!(condition.isCompilable() && left.isCompilable() && right.isCompilable())) { + return false; + } + return CodeFlow.isBooleanCompatible(condition.exitTypeDescriptor) && + left.getExitDescriptor()!=null && + right.getExitDescriptor()!=null; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + // May reach here without it computed if all elements are literals + computeExitTypeDescriptor(); + codeflow.enterCompilationScope(); + this.children[0].generateCode(mv, codeflow); + codeflow.exitCompilationScope(); + Label elseTarget = new Label(); + Label endOfIf = new Label(); + mv.visitJumpInsn(IFEQ, elseTarget); + codeflow.enterCompilationScope(); + this.children[1].generateCode(mv, codeflow); + if (!CodeFlow.isPrimitive(getExitDescriptor())) { + CodeFlow.insertBoxIfNecessary(mv, codeflow.lastDescriptor().charAt(0)); + } + codeflow.exitCompilationScope(); + mv.visitJumpInsn(GOTO, endOfIf); + mv.visitLabel(elseTarget); + codeflow.enterCompilationScope(); + this.children[2].generateCode(mv, codeflow); + if (!CodeFlow.isPrimitive(getExitDescriptor())) { + CodeFlow.insertBoxIfNecessary(mv, codeflow.lastDescriptor().charAt(0)); + } + codeflow.exitCompilationScope(); + mv.visitLabel(endOfIf); + codeflow.pushDescriptor(getExitDescriptor()); + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java index ade0d20f315..963658294c6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,12 @@ package org.springframework.expression.spel.ast; import java.lang.reflect.Array; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Type; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.standard.CodeFlow; /** * Represents a reference to a type, for example "T(String)" or "T(com.somewhere.Foo)" @@ -31,6 +34,7 @@ public class TypeReference extends SpelNodeImpl { private final int dimensions; + private transient Class type; public TypeReference(int pos, SpelNodeImpl qualifiedId) { this(pos,qualifiedId,0); @@ -52,11 +56,15 @@ public class TypeReference extends SpelNodeImpl { // it is a primitive type Class clazz = tc.getType(); clazz = makeArrayIfNecessary(clazz); + this.exitTypeDescriptor = "Ljava/lang/Class"; + this.type = clazz; return new TypedValue(clazz); } } Class clazz = state.findType(typename); clazz = makeArrayIfNecessary(clazz); + this.exitTypeDescriptor = "Ljava/lang/Class"; + this.type = clazz; return new TypedValue(clazz); } @@ -81,5 +89,40 @@ public class TypeReference extends SpelNodeImpl { sb.append(")"); return sb.toString(); } + + @Override + public boolean isCompilable() { + return this.exitTypeDescriptor != null; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + // TODO Future optimization - if followed by a static method call, skip generating code here + if (type.isPrimitive()) { + if (type == Integer.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Integer", "TYPE", "Ljava/lang/Class;"); + } else if (type == Boolean.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;"); + } else if (type == Byte.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Byte", "TYPE", "Ljava/lang/Class;"); + } else if (type == Short.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Short", "TYPE", "Ljava/lang/Class;"); + } else if (type == Double.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Double", "TYPE", "Ljava/lang/Class;"); + } else if (type == Character.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Character", "TYPE", "Ljava/lang/Class;"); + } else if (type == Float.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Float", "TYPE", "Ljava/lang/Class;"); + } else if (type == Long.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Long", "TYPE", "Ljava/lang/Class;"); + } else if (type == Boolean.TYPE) { + mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TYPE", "Ljava/lang/Class;"); + } + } + else { + mv.visitLdcInsn(Type.getType(type)); + } + codeflow.pushDescriptor(getExitDescriptor()); + } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java index 607c70c389d..6cbe3e1e30d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,12 @@ package org.springframework.expression.spel.ast; +import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.standard.CodeFlow; /** * Represents a variable reference, eg. #someVar. Note this is different to a *local* @@ -64,9 +66,12 @@ public class VariableReference extends SpelNodeImpl { return state.getActiveContextObject(); } if (this.name.equals(ROOT)) { - return state.getRootContextObject(); + TypedValue result = state.getRootContextObject(); + this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(result.getValue()); + return result; } TypedValue result = state.lookupVariable(this.name); + this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(result.getValue()); // a null value will mean either the value was null or the variable was not found return result; } @@ -120,5 +125,23 @@ public class VariableReference extends SpelNodeImpl { } } + @Override + public boolean isCompilable() { + return getExitDescriptor()!=null; + } + + @Override + public void generateCode(MethodVisitor mv, CodeFlow codeflow) { + if (this.name.equals(ROOT)) { + mv.visitVarInsn(ALOAD,1); + } else { + mv.visitVarInsn(ALOAD, 2); + mv.visitLdcInsn(name); + mv.visitMethodInsn(INVOKEINTERFACE, "org/springframework/expression/EvaluationContext", "lookupVariable", "(Ljava/lang/String;)Ljava/lang/Object;",true); + } + CodeFlow.insertCheckCast(mv,getExitDescriptor()); + codeflow.pushDescriptor(getExitDescriptor()); + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/CodeFlow.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/CodeFlow.java new file mode 100644 index 00000000000..4cc1f1ad407 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/CodeFlow.java @@ -0,0 +1,603 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression.spel.standard; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Stack; + +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.util.Assert; + +/** + * Records intermediate compilation state as the bytecode is generated for a parsed + * expression. Also contains bytecode generation helper functions. + * + * @author Andy Clement + * @since 4.1 + */ +public class CodeFlow implements Opcodes { + + /** + * Record the type of what is on top of the bytecode stack (i.e. the type of the + * output from the previous expression component). New scopes are used to evaluate + * sub-expressions like the expressions for the argument values in a method invocation + * expression. + */ + private Stack> compilationScopes; + + public CodeFlow() { + compilationScopes = new Stack>(); + compilationScopes.add(new ArrayList()); + } + + /** + * Push the byte code to load the target (i.e. what was passed as the first argument + * to CompiledExpression.getValue(target, context)) + * @param mv the visitor into which the load instruction should be inserted + */ + public void loadTarget(MethodVisitor mv) { + mv.visitVarInsn(ALOAD, 1); + } + + /** + * Record the descriptor for the most recently evaluated expression element. + * @param descriptor type descriptor for most recently evaluated element + */ + public void pushDescriptor(String descriptor) { + Assert.notNull(descriptor); + compilationScopes.peek().add(descriptor); + } + + /** + * Enter a new compilation scope, usually due to nested expression evaluation. For + * example when the arguments for a method invocation expression are being evaluated, + * each argument will be evaluated in a new scope. + */ + public void enterCompilationScope() { + compilationScopes.push(new ArrayList()); + } + + /** + * Exit a compilation scope, usually after a nested expression has been evaluated. For + * example after an argument for a method invocation has been evaluated this method + * returns us to the previous (outer) scope. + */ + public void exitCompilationScope() { + compilationScopes.pop(); + } + + /** + * @return the descriptor for the item currently on top of the stack (in the current + * scope) + */ + public String lastDescriptor() { + if (compilationScopes.peek().size()==0) { + return null; + } + return compilationScopes.peek().get(compilationScopes.peek().size()-1); + } + + /** + * If the codeflow shows the last expression evaluated to java.lang.Boolean then + * insert the necessary instructions to unbox that to a boolean primitive. + * @param mv the visitor into which new instructions should be inserted + */ + public void unboxBooleanIfNecessary(MethodVisitor mv) { + if (lastDescriptor().equals("Ljava/lang/Boolean")) { + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false); + } + } + + /** + * Insert any necessary cast and value call to convert from a boxed type to a + * primitive value + * @param mv the method visitor into which instructions should be inserted + * @param ch the primitive type desired as output + * @param isObject indicates whether the type on the stack is being thought of as + * Object (and so requires a cast) + */ + public static void insertUnboxInsns(MethodVisitor mv, char ch, boolean isObject) { + switch (ch) { + case 'I': + if (isObject) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Integer"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false); + break; + case 'Z': + if (isObject) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Boolean"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z", false); + break; + case 'B': + if (isObject) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Byte"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false); + break; + case 'C': + if (isObject) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Character"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C", false); + break; + case 'D': + if (isObject) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Double"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D", false); + break; + case 'S': + if (isObject) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Short"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false); + break; + case 'F': + if (isObject) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Float"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false); + break; + case 'J': + if (isObject) { + mv.visitTypeInsn(CHECKCAST, "java/lang/Long"); + } + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false); + break; + default: + throw new IllegalArgumentException("Unboxing should not be attempted for descriptor '" + ch + "'"); + } + } + + /** + * Create the JVM signature descriptor for a method. This consists of the descriptors + * for the constructor parameters surrounded with parentheses, followed by the + * descriptor for the return type. Note the descriptors here are JVM descriptors, + * unlike the other descriptor forms the compiler is using which do not include the + * trailing semicolon. + * @param method the method + * @return a String signature descriptor (e.g. "(ILjava/lang/String;)V") + */ + public static String createSignatureDescriptor(Method method) { + Class[] params = method.getParameterTypes(); + StringBuilder s = new StringBuilder(); + s.append("("); + for (int i = 0, max = params.length; i < max; i++) { + s.append(toJVMDescriptor(params[i])); + } + s.append(")"); + s.append(toJVMDescriptor(method.getReturnType())); + return s.toString(); + } + + /** + * Create the JVM signature descriptor for a constructor. This consists of the + * descriptors for the constructor parameters surrounded with parentheses. Note the + * descriptors here are JVM descriptors, unlike the other descriptor forms the + * compiler is using which do not include the trailing semicolon. + * @param ctor the constructor + * @return a String signature descriptor (e.g. "(ILjava/lang/String;)") + */ + public static String createSignatureDescriptor(Constructor ctor) { + Class[] params = ctor.getParameterTypes(); + StringBuilder s = new StringBuilder(); + s.append("("); + for (int i = 0, max = params.length; i < max; i++) { + s.append(toJVMDescriptor(params[i])); + } + s.append(")V"); + return s.toString(); + } + + /** + * Determine the JVM descriptor for a specified class. Unlike the other descriptors + * used in the compilation process, this is the one the JVM wants, so this one + * includes any necessary trailing semicolon (e.g. Ljava/lang/String; rather than + * Ljava/lang/String) + * + * @param clazz a class + * @return the JVM descriptor for the class + */ + public static String toJVMDescriptor(Class clazz) { + StringBuilder s= new StringBuilder(); + if (clazz.isArray()) { + while (clazz.isArray()) { + s.append("["); + clazz = clazz.getComponentType(); + } + } + if (clazz.isPrimitive()) { + if (clazz == Void.TYPE) { + s.append('V'); + } + else if (clazz == Integer.TYPE) { + s.append('I'); + } + else if (clazz == Boolean.TYPE) { + s.append('Z'); + } + else if (clazz == Character.TYPE) { + s.append('C'); + } + else if (clazz == Long.TYPE) { + s.append('J'); + } + else if (clazz == Double.TYPE) { + s.append('D'); + } + else if (clazz == Float.TYPE) { + s.append('F'); + } + else if (clazz == Byte.TYPE) { + s.append('B'); + } + else if (clazz == Short.TYPE) { + s.append('S'); + } + } else { + s.append("L"); + s.append(clazz.getName().replace('.', '/')); + s.append(";"); + } + return s.toString(); + } + + /** + * Determine the descriptor for an object instance (or null). + * @param value an object (possibly null) + * @return the type descriptor for the object (descriptor is "Ljava/lang/Object" for + * null value) + */ + public static String toDescriptorFromObject(Object value) { + if (value == null) { + return "Ljava/lang/Object"; + } else { + return toDescriptor(value.getClass()); + } + } + + /** + * @param descriptor type descriptor + * @return true if the descriptor is for a boolean primitive or boolean reference type + */ + public static boolean isBooleanCompatible(String descriptor) { + return descriptor != null + && (descriptor.equals("Z") || descriptor.equals("Ljava/lang/Boolean")); + } + + /** + * @param descriptor type descriptor + * @return true if the descriptor is for a primitive type + */ + public static boolean isPrimitive(String descriptor) { + return descriptor!=null && descriptor.length()==1; + } + + /** + * @param descriptor the descriptor for a possible primitive array + * @return true if the descriptor is for a primitive array (e.g. "[[I") + */ + public static boolean isPrimitiveArray(String descriptor) { + boolean primitive = true; + for (int i = 0, max = descriptor.length(); i < max; i++) { + char ch = descriptor.charAt(i); + if (ch == '[') { + continue; + } + primitive = (ch != 'L'); + break; + } + return primitive; + } + + /** + * Determine if boxing/unboxing can get from one type to the other. Assumes at least + * one of the types is in boxed form (i.e. single char descriptor). + * + * @return true if it is possible to get (via boxing) from one descriptor to the other + */ + public static boolean areBoxingCompatible(String desc1, String desc2) { + if (desc1.equals(desc2)) { + return true; + } + if (desc1.length()==1) { + if (desc1.equals("D")) { + return desc2.equals("Ljava/lang/Double"); + } + else if (desc1.equals("F")) { + return desc2.equals("Ljava/lang/Float"); + } + else if (desc1.equals("J")) { + return desc2.equals("Ljava/lang/Long"); + } + else if (desc1.equals("I")) { + return desc2.equals("Ljava/lang/Integer"); + } + else if (desc1.equals("Z")) { + return desc2.equals("Ljava/lang/Boolean"); + } + } + else if (desc2.length()==1) { + if (desc2.equals("D")) { + return desc1.equals("Ljava/lang/Double"); + } + else if (desc2.equals("F")) { + return desc1.equals("Ljava/lang/Float"); + } + else if (desc2.equals("J")) { + return desc1.equals("Ljava/lang/Long"); + } + else if (desc2.equals("I")) { + return desc1.equals("Ljava/lang/Integer"); + } + else if (desc2.equals("Z")) { + return desc1.equals("Ljava/lang/Boolean"); + } + } + return false; + } + + /** + * Determine if the supplied descriptor is for a supported number type or boolean. The + * compilation process only (currently) supports certain number types. These are + * double, float, long and int. + * @param descriptor the descriptor for a type + * @return true if the descriptor is for a supported numeric type or boolean + */ + public static boolean isPrimitiveOrUnboxableSupportedNumberOrBoolean(String descriptor) { + if (descriptor==null) { + return false; + } + if (descriptor.length()==1) { + return "DFJZI".indexOf(descriptor.charAt(0))!=-1; + } + if (descriptor.startsWith("Ljava/lang/")) { + if (descriptor.equals("Ljava/lang/Double") || descriptor.equals("Ljava/lang/Integer") || + descriptor.equals("Ljava/lang/Float") || descriptor.equals("Ljava/lang/Long") || + descriptor.equals("Ljava/lang/Boolean")) { + return true; + } + } + return false; + } + + /** + * Determine if the supplied descriptor is for a supported number. The compilation + * process only (currently) supports certain number types. These are double, float, + * long and int. + * @param descriptor the descriptor for a type + * @return true if the descriptor is for a supported numeric type + */ + public static boolean isPrimitiveOrUnboxableSupportedNumber(String descriptor) { + if (descriptor==null) { + return false; + } + if (descriptor.length()==1) { + return "DFJI".indexOf(descriptor.charAt(0))!=-1; + } + if (descriptor.startsWith("Ljava/lang/")) { + if (descriptor.equals("Ljava/lang/Double") || descriptor.equals("Ljava/lang/Integer") || + descriptor.equals("Ljava/lang/Float") || descriptor.equals("Ljava/lang/Long")) { + return true; + } + } + return false; + } + + /** + * @param descriptor a descriptor for a type that should have a primitive + * representation + * @return the single character descriptor for a primitive input descriptor + */ + public static char toPrimitiveTargetDesc(String descriptor) { + if (descriptor.length()==1) { + return descriptor.charAt(0); + } + if (descriptor.equals("Ljava/lang/Double")) { + return 'D'; + } + else if (descriptor.equals("Ljava/lang/Integer")) { + return 'I'; + } + else if (descriptor.equals("Ljava/lang/Float")) { + return 'F'; + } + else if (descriptor.equals("Ljava/lang/Long")) { + return 'J'; + } + else if (descriptor.equals("Ljava/lang/Boolean")) { + return 'Z'; + } + else { + throw new IllegalStateException("No primitive for '"+descriptor+"'"); + } + } + + /** + * Insert the appropriate CHECKCAST instruction for the supplied descriptor. + * @param mv the target visitor into which the instruction should be inserted + * @param descriptor the descriptor of the type to cast to + */ + public static void insertCheckCast(MethodVisitor mv, String descriptor) { + if (descriptor.length()!=1) { + if (descriptor.charAt(0)=='[') { + if (CodeFlow.isPrimitiveArray(descriptor)) { + mv.visitTypeInsn(CHECKCAST, descriptor); + } + else { + mv.visitTypeInsn(CHECKCAST, descriptor+";"); + } + } + else { + // This is chopping off the 'L' to leave us with "java/lang/String" + mv.visitTypeInsn(CHECKCAST, descriptor.substring(1)); + } + } + } + + /** + * Determine the appropriate boxing instruction for a specific type (if it needs + * boxing) and insert the instruction into the supplied visitor. + * @param mv the target visitor for the new instructions + * @param descriptor the descriptor of a type that may or may not need boxing + */ + public static void insertBoxIfNecessary(MethodVisitor mv, String descriptor) { + if (descriptor.length() == 1) { + insertBoxIfNecessary(mv, descriptor.charAt(0)); + } + } + + /** + * Determine the appropriate boxing instruction for a specific type (if it needs + * boxing) and insert the instruction into the supplied visitor. + * @param mv the target visitor for the new instructions + * @param ch the descriptor of the type that might need boxing + */ + public static void insertBoxIfNecessary(MethodVisitor mv, char ch) { + switch (ch) { + case 'I': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false); + break; + case 'F': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf", "(F)Ljava/lang/Float;", false); + break; + case 'S': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf", "(S)Ljava/lang/Short;", false); + break; + case 'Z': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false); + break; + case 'J': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf", "(J)Ljava/lang/Long;", false); + break; + case 'D': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf", "(D)Ljava/lang/Double;", false); + break; + case 'C': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf", "(C)Ljava/lang/Character;", false); + break; + case 'B': + mv.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf", "(B)Ljava/lang/Byte;", false); + break; + case 'L': + case 'V': + case '[': + // no box needed + break; + default: + throw new IllegalArgumentException("Boxing should not be attempted for descriptor '" + ch + "'"); + } + } + + /** + * Deduce the descriptor for a type. Descriptors are like JVM type names but missing + * the trailing ';' so for Object the descriptor is "Ljava/lang/Object" for int it is + * "I". + * @param type the type (may be primitive) for which to determine the descriptor + * @return the descriptor + */ + public static String toDescriptor(Class type) { + String name = type.getName(); + if (type.isPrimitive()) { + switch (name.length()) { + case 3: + return "I"; + case 4: + if (name.equals("long")) { + return "J"; + } + else if (name.equals("char")) { + return "C"; + } + else if (name.equals("byte")) { + return "B"; + } + else if (name.equals("void")) { + return "V"; + } + break; + case 5: + if (name.equals("float")) { + return "F"; + } + else if (name.equals("short")) { + return "S"; + } + break; + case 6: + if (name.equals("double")) { + return "D"; + } + break; + case 7: + if (name.equals("boolean")) { + return "Z"; + } + break; + } + } + else { + if (name.charAt(0) != '[') { + return new StringBuilder("L").append(type.getName().replace('.', '/')).toString(); + } + else { + if (name.endsWith(";")) { + return name.substring(0, name.length() - 1).replace('.', '/'); + } + else { + return name; // array has primitive component type + } + } + } + return null; + } + + /** + * Create an array of descriptors representing the parameter types for the supplied + * method. Returns a zero sized array if there are no parameters. + * @param method a Method + * @return a String array of descriptors, one entry for each method parameter + */ + public static String[] toParamDescriptors(Method method) { + return toDescriptors(method.getParameterTypes()); + } + + /** + * Create an array of descriptors representing the parameter types for the supplied + * constructor. Returns a zero sized array if there are no parameters. + * @param ctor a Constructor + * @return a String array of descriptors, one entry for each constructor parameter + */ + public static String[] toParamDescriptors(Constructor ctor) { + return toDescriptors(ctor.getParameterTypes()); + } + + private static String[] toDescriptors(Class[] types) { + int typesCount = types.length; + String[] descriptors = new String[typesCount]; + for (int p = 0; p < typesCount; p++) { + descriptors[p] = CodeFlow.toDescriptor(types[p]); + } + return descriptors; + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java new file mode 100644 index 00000000000..5d32b6395db --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java @@ -0,0 +1,277 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.expression.spel.standard; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; + +import org.springframework.asm.ClassWriter; +import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.CompiledExpression; +import org.springframework.expression.spel.SpelParserConfiguration; +import org.springframework.expression.spel.ast.SpelNodeImpl; +import org.springframework.util.ClassUtils; + +/** + * A SpelCompiler will take a regular parsed expression and create (and load) a class + * containing byte code that does the same thing as that expression. The compiled form of + * an expression will evaluate far faster than the interpreted form. + *

+ * The SpelCompiler is not currently handling all expression types but covers many of the + * common cases. The framework is extensible to cover more cases in the future. For + * absolute maximum speed there is *no checking* in the compiled code. The compiled + * version of the expression uses information learned during interpreted runs of the + * expression when it generates the byte code. For example if it knows that a particular + * property dereference always seems to return a Map then it will generate byte code that + * expects the result of the property dereference to be a Map. This ensures maximal + * performance but should the dereference result in something other than a map, the + * compiled expression will fail - like a ClassCastException would occur if passing data + * of an unexpected type in a regular Java program. + *

+ * Due to the lack of checking there are likely some expressions that should never be + * compiled, for example if an expression is continuously dealing with different types of + * data. Due to these cases the compiler is something that must be selectively turned on + * for an associated SpelExpressionParser (through the {@link SpelParserConfiguration} + * object), it is not on by default. + *

+ * Individual expressions can be compiled by calling + * SpelCompiler.compile(expression). + * + * @author Andy Clement + * @since 4.1 + */ +public class SpelCompiler implements Opcodes { + + // Default number of times to interpret an expression before compiling it + private static int DEFAULT_INTERPRETED_COUNT_THRESHOLD = 100; + + // Once an expression is evaluated the threshold number of times, it will be a candidate for compilation + public static int interpretedCountThreshold = DEFAULT_INTERPRETED_COUNT_THRESHOLD; + + // Useful for debugging + public static final boolean verbose = false; + + // A compiler is created for each classloader, it manages a child class loader of that + // classloader and the child is used to load the compiled expressions. + private static Map compilers = Collections.synchronizedMap(new WeakHashMap()); + + // The child classloader used to load the compiled expression classes + private ChildClassLoader ccl; + + // counter suffix for generated classes within this SpelCompiler instance + private int suffixId; + + /** + * Factory method for compiler instances. The returned SpelCompiler will + * attach a class loader as the child of the default class loader and this + * child will be used to load compiled expressions. + * + * @return a SpelCompiler instance + */ + public static SpelCompiler getCompiler() { + ClassLoader classloader = ClassUtils.getDefaultClassLoader(); + synchronized (compilers) { + SpelCompiler compiler = compilers.get(classloader); + if (compiler == null) { + compiler = new SpelCompiler(classloader); + compilers.put(classloader,compiler); + } + return compiler; + } + } + + private SpelCompiler(ClassLoader classloader) { + this.ccl = new ChildClassLoader(classloader); + this.suffixId = 1; + } + + /** + * Attempt compilation of the supplied expression. A check is + * made to see if it is compilable before compilation proceeds. The + * check involves visiting all the nodes in the expression Ast and + * ensuring enough state is known about them that bytecode can + * be generated for them. + * @param expression the expression to compile + * @return an instance of the class implementing the compiled expression, or null + * if compilation is not possible + */ + public CompiledExpression compile(SpelNodeImpl expression) { + if (expression.isCompilable()) { + if (verbose) { + System.out.println("SpEL: compiling " + expression.toStringAST()); + } + Class clazz = createExpressionClass(expression); + try { + CompiledExpression instance = clazz.newInstance(); + return instance; + } + catch (InstantiationException ie) { + ie.printStackTrace(); + } + catch (IllegalAccessException iae) { + iae.printStackTrace(); + } + } + else { + if (verbose) { + System.out.println("SpEL: unable to compile " + expression.toStringAST()); + } + } + return null; + } + + private synchronized int getNextSuffix() { + return suffixId++; + } + + /** + * Generate the class that encapsulates the compiled expression and define it. The + * generated class will be a subtype of CompiledExpression. + * @param expressionToCompile the expression to be compiled + */ + @SuppressWarnings("unchecked") + private Class createExpressionClass(SpelNodeImpl expressionToCompile) { + + // Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression' + String clazzName = "spel/Ex" + getNextSuffix(); + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS|ClassWriter.COMPUTE_FRAMES); + cw.visit(V1_5, ACC_PUBLIC, clazzName, null, + "org/springframework/expression/spel/CompiledExpression", null); + + // Create default constructor + MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); + mv.visitCode(); + mv.visitVarInsn(ALOAD, 0); + mv.visitMethodInsn(INVOKESPECIAL, "org/springframework/expression/spel/CompiledExpression", "", "()V",false); + mv.visitInsn(RETURN); + mv.visitMaxs(1, 1); + mv.visitEnd(); + + // Create getValue() method + mv = cw.visitMethod(ACC_PUBLIC, "getValue", "(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null, + new String[]{"org/springframework/expression/EvaluationException"}); + mv.visitCode(); + + CodeFlow codeflow = new CodeFlow(); + + // Ask the expression Ast to generate the body of the method + expressionToCompile.generateCode(mv,codeflow); + + CodeFlow.insertBoxIfNecessary(mv,codeflow.lastDescriptor()); + if (codeflow.lastDescriptor() == "V") { + mv.visitInsn(ACONST_NULL); + } + mv.visitInsn(ARETURN); + + mv.visitMaxs(0,0); // not supplied due to COMPUTE_MAXS + mv.visitEnd(); + cw.visitEnd(); + byte[] data = cw.toByteArray(); + // TODO need to make this conditionally occur based on a debug flag + // dump(expressionToCompile.toStringAST(), clazzName, data); + Class clazz = (Class) ccl.defineClass(clazzName.replaceAll("/","."),data); + return clazz; + } + + /** + * For debugging purposes, dump the specified byte code into a file on the disk. Not + * yet hooked in, needs conditionally calling based on a sys prop. + * + * @param expressionText the text of the expression compiled + * @param name the name of the class being used for the compiled expression + * @param bytecode the bytecode for the generated class + */ + @SuppressWarnings("unused") + private static void dump(String expressionText, String name, byte[] bytecode) { + name = name.replace('.', '/'); + String dir = ""; + if (name.indexOf('/') != -1) { + dir = name.substring(0, name.lastIndexOf('/')); + } + String dumplocation = null; + try { + File tempfile = null; + tempfile = File.createTempFile("tmp", null); + tempfile.delete(); + File f = new File(tempfile, dir); + f.mkdirs(); + dumplocation = tempfile + File.separator + name + ".class"; + System.out.println("Expression '" + expressionText + "' compiled code dumped to " + + dumplocation); + f = new File(dumplocation); + FileOutputStream fos = new FileOutputStream(f); + fos.write(bytecode); + fos.flush(); + fos.close(); + } + catch (IOException ioe) { + throw new IllegalStateException("Unexpected problem dumping class " + + name + " into " + dumplocation, ioe); + } + } + + + /** + * A ChildClassLoader will load the generated compiled expression classes + */ + public static class ChildClassLoader extends URLClassLoader { + + private static URL[] NO_URLS = new URL[0]; + + public ChildClassLoader(ClassLoader classloader) { + super(NO_URLS, classloader); + } + + public Class defineClass(String name, byte[] bytes) { + return super.defineClass(name, bytes, 0, bytes.length); + } + + } + + /** + * Request that an attempt is made to compile the specified expression. It may fail if + * components of the expression are not suitable for compilation or the data types + * involved are not suitable for compilation. Used for testing. + * @return true if the expression was successfully compiled + */ + public static boolean compile(Expression expression) { + if (expression instanceof SpelExpression) { + SpelExpression spelExpression = (SpelExpression)expression; + return spelExpression.compileExpression(); + } + return false; + } + + /** + * Request to revert to the interpreter for expression evaluation. Any compiled form + * is discarded but can be recreated by later recompiling again. + * @param expression the expression + */ + public static void revertToInterpreted(Expression expression) { + if (expression instanceof SpelExpression) { + SpelExpression spelExpression = (SpelExpression)expression; + spelExpression.revertToInterpreted(); + } + } +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java index 18afa0be62e..7d04a14b310 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,11 @@ import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.TypedValue; import org.springframework.expression.common.ExpressionUtils; +import org.springframework.expression.spel.CompiledExpression; import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelCompilerMode; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.SpelNode; import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.ast.SpelNodeImpl; @@ -42,13 +46,24 @@ public class SpelExpression implements Expression { private final String expression; - private final SpelNodeImpl ast; + // Holds the compiled form of the expression (if it has been compiled) + private CompiledExpression compiledAst; + + private SpelNodeImpl ast; private final SpelParserConfiguration configuration; // the default context is used if no override is supplied by the user private EvaluationContext defaultContext; + // Count of many times as the expression been interpreted - can trigger compilation + // when certain limit reached + private int interpretedCount = 0; + + // The number of times compilation was attempted and failed - enables us to eventually + // give up trying to compile it when it just doesn't seem to be possible. + private int failedAttempts = 0; + /** * Construct an expression, only used by the parser. @@ -64,51 +79,214 @@ public class SpelExpression implements Expression { @Override public Object getValue() throws EvaluationException { + Object result = null; + if (compiledAst != null) { + try { + return this.compiledAst.getValue(null,null); + } catch (Throwable t) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.mixed) { + interpretedCount = 0; + compiledAst = null; + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(t,SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } ExpressionState expressionState = new ExpressionState(getEvaluationContext(), this.configuration); - return this.ast.getValue(expressionState); + result = this.ast.getValue(expressionState); + checkCompile(expressionState); + return result; } @Override public Object getValue(Object rootObject) throws EvaluationException { + Object result = null; + if (compiledAst!=null) { + try { + return this.compiledAst.getValue(rootObject,null); + } catch (Throwable t) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.mixed) { + interpretedCount = 0; + compiledAst = null; + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(t,SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } ExpressionState expressionState = new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration); - return this.ast.getValue(expressionState); + result = this.ast.getValue(expressionState); + checkCompile(expressionState); + return result; } + @SuppressWarnings("unchecked") @Override public T getValue(Class expectedResultType) throws EvaluationException { + if (compiledAst!=null) { + try { + Object result = this.compiledAst.getValue(null,null); + if (expectedResultType == null) { + return (T)result; + } else { + return ExpressionUtils.convertTypedValue(getEvaluationContext(), new TypedValue(result), expectedResultType); + } + } catch (Throwable t) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.mixed) { + interpretedCount = 0; + compiledAst = null; + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(t,SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } ExpressionState expressionState = new ExpressionState(getEvaluationContext(), this.configuration); TypedValue typedResultValue = this.ast.getTypedValue(expressionState); + checkCompile(expressionState); return ExpressionUtils.convertTypedValue(expressionState.getEvaluationContext(), typedResultValue, expectedResultType); } + @SuppressWarnings("unchecked") @Override public T getValue(Object rootObject, Class expectedResultType) throws EvaluationException { + if (compiledAst!=null) { + try { + Object result = this.compiledAst.getValue(rootObject,null); + if (expectedResultType == null) { + return (T)result; + } else { + return ExpressionUtils.convertTypedValue(getEvaluationContext(), new TypedValue(result), expectedResultType); + } + } catch (Throwable t) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.mixed) { + interpretedCount = 0; + compiledAst = null; + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(t,SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } ExpressionState expressionState = new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration); TypedValue typedResultValue = this.ast.getTypedValue(expressionState); + checkCompile(expressionState); return ExpressionUtils.convertTypedValue(expressionState.getEvaluationContext(), typedResultValue, expectedResultType); } @Override public Object getValue(EvaluationContext context) throws EvaluationException { Assert.notNull(context, "The EvaluationContext is required"); - return this.ast.getValue(new ExpressionState(context, this.configuration)); + if (compiledAst!= null) { + try { + Object result = this.compiledAst.getValue(null,context); + return result; + } catch (Throwable t) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.mixed) { + interpretedCount = 0; + compiledAst = null; + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(t,SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + ExpressionState expressionState = new ExpressionState(context, this.configuration); + Object result = this.ast.getValue(expressionState); + checkCompile(expressionState); + return result; } @Override public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException { Assert.notNull(context, "The EvaluationContext is required"); - return this.ast.getValue(new ExpressionState(context, toTypedValue(rootObject), this.configuration)); + if (compiledAst!=null) { + try { + return this.compiledAst.getValue(rootObject,context); + } catch (Throwable t) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.mixed) { + interpretedCount = 0; + compiledAst = null; + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(t,SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + ExpressionState expressionState = new ExpressionState(context, toTypedValue(rootObject), this.configuration); + Object result = this.ast.getValue(expressionState); + checkCompile(expressionState); + return result; } + @SuppressWarnings("unchecked") @Override public T getValue(EvaluationContext context, Class expectedResultType) throws EvaluationException { - TypedValue typedResultValue = this.ast.getTypedValue(new ExpressionState(context, this.configuration)); + if (compiledAst!=null) { + try { + Object result = this.compiledAst.getValue(null,context); + if (expectedResultType!=null) { + return (T) result; + } else { + return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType); + } + } catch (Throwable t) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.mixed) { + interpretedCount = 0; + compiledAst = null; + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(t,SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + ExpressionState expressionState = new ExpressionState(context, this.configuration); + TypedValue typedResultValue = this.ast.getTypedValue(expressionState); + checkCompile(expressionState); return ExpressionUtils.convertTypedValue(context, typedResultValue, expectedResultType); } + @SuppressWarnings("unchecked") @Override public T getValue(EvaluationContext context, Object rootObject, Class expectedResultType) throws EvaluationException { - TypedValue typedResultValue = this.ast.getTypedValue(new ExpressionState(context, toTypedValue(rootObject), this.configuration)); + if (compiledAst!=null) { + try { + Object result = this.compiledAst.getValue(rootObject,context); + if (expectedResultType!=null) { + return (T) result; + } else { + return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType); + } + } catch (Throwable t) { + // If running in mixed mode, revert to interpreted + if (this.configuration.getCompilerMode() == SpelCompilerMode.mixed) { + interpretedCount = 0; + compiledAst = null; + } + else { + // Running in SpelCompilerMode.immediate mode - propagate exception to caller + throw new SpelEvaluationException(t,SpelMessage.EXCEPTION_RUNNING_COMPILED_EXPRESSION); + } + } + } + ExpressionState expressionState = new ExpressionState(context, toTypedValue(rootObject), this.configuration); + TypedValue typedResultValue = this.ast.getTypedValue(expressionState); + checkCompile(expressionState); return ExpressionUtils.convertTypedValue(context, typedResultValue, expectedResultType); } @@ -203,6 +381,66 @@ public class SpelExpression implements Expression { // impl only + /** + * Compile the expression if it has been evaluated more than the threshold number of times to trigger compilation. + * @param expressionState the expression state used to determine compilation mode + */ + private void checkCompile(ExpressionState expressionState) { + interpretedCount++; + SpelCompilerMode compilerMode = expressionState.getConfiguration().getCompilerMode(); + if (compilerMode != SpelCompilerMode.off) { + if (compilerMode == SpelCompilerMode.immediate) { + if (interpretedCount > 1) { + compileExpression(); + } + } + else { + // compilerMode = SpelCompilerMode.mixed + if (interpretedCount > SpelCompiler.interpretedCountThreshold) { + compileExpression(); + } + } + } + } + + + /** + * Perform expression compilation. This will only succeed once exit descriptors for all nodes have + * been determined. If the compilation fails and has failed more than 100 times the expression is + * no longer considered suitable for compilation. + */ + public boolean compileExpression() { + if (failedAttempts > 100) { + // Don't try again + return false; + } + if (this.compiledAst == null) { + synchronized (expression) { + // Possibly compiled by another thread before this thread got into the + // sync block + if (this.compiledAst != null) { + return true; + } + this.compiledAst = SpelCompiler.getCompiler().compile(this.ast); + if (this.compiledAst == null) { + failedAttempts++; + } + } + } + return (this.compiledAst != null); + } + + /** + * Cause an expression to revert to being interpreted if it has been using a compiled + * form. It also resets the compilation attempt failure count (an expression is normally no + * longer considered compilable if it cannot be compiled after 100 attempts). + */ + public void revertToInterpreted() { + this.compiledAst = null; + this.interpretedCount = 0; + this.failedAttempts = 0; + } + /** * @return return the Abstract Syntax Tree for the expression */ diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index 66fd45551f0..263bceed7a0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -228,16 +228,18 @@ public class ReflectionHelper { * @param arguments the actual arguments that need conversion * @param methodOrCtor the target Method or Constructor * @param varargsPosition the known position of the varargs argument, if any + * @return true if some kind of conversion occurred on an argument * @throws EvaluationException if a problem occurs during conversion */ - static void convertArguments(TypeConverter converter, Object[] arguments, Object methodOrCtor, + static boolean convertArguments(TypeConverter converter, Object[] arguments, Object methodOrCtor, Integer varargsPosition) throws EvaluationException { - + boolean conversionOccurred = false; if (varargsPosition == null) { for (int i = 0; i < arguments.length; i++) { TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forMethodOrConstructor(methodOrCtor, i)); Object argument = arguments[i]; arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + conversionOccurred |= (argument != arguments[i]); } } else { @@ -245,21 +247,25 @@ public class ReflectionHelper { TypeDescriptor targetType = new TypeDescriptor(MethodParameter.forMethodOrConstructor(methodOrCtor, i)); Object argument = arguments[i]; arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + conversionOccurred |= (argument != arguments[i]); } MethodParameter methodParam = MethodParameter.forMethodOrConstructor(methodOrCtor, varargsPosition); if (varargsPosition == arguments.length - 1) { TypeDescriptor targetType = new TypeDescriptor(methodParam); Object argument = arguments[varargsPosition]; arguments[varargsPosition] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + conversionOccurred |= (argument != arguments[varargsPosition]); } else { TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor(); for (int i = varargsPosition; i < arguments.length; i++) { Object argument = arguments[i]; arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + conversionOccurred |= (argument != arguments[i]); } } } + return conversionOccurred; } /** @@ -272,15 +278,16 @@ public class ReflectionHelper { * @param converter the converter to use for type conversions * @param arguments the arguments to convert to the requested parameter types * @param method the target Method + * @return true if some kind of conversion occurred on the argument * @throws SpelEvaluationException if there is a problem with conversion */ - public static void convertAllArguments(TypeConverter converter, Object[] arguments, Method method) - throws SpelEvaluationException { - + public static boolean convertAllArguments(TypeConverter converter, Object[] arguments, Method method) throws SpelEvaluationException { Integer varargsPosition = null; + boolean conversionOccurred = false; if (method.isVarArgs()) { Class[] paramTypes = method.getParameterTypes(); varargsPosition = paramTypes.length - 1; + conversionOccurred = true; } for (int argPos = 0; argPos < arguments.length; argPos++) { TypeDescriptor targetType; @@ -299,6 +306,7 @@ public class ReflectionHelper { SpelMessage.TYPE_CONVERSION_ERROR, argument.getClass().getName(), targetType); } arguments[argPos] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType); + conversionOccurred |= (argument != arguments[argPos]); } } catch (EvaluationException ex) { @@ -312,6 +320,7 @@ public class ReflectionHelper { } } } + return conversionOccurred; } /** diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java index cb886ecc92b..645f0a3fe31 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,7 @@ import org.springframework.util.ReflectionUtils; * @author Juergen Hoeller * @since 3.0 */ -class ReflectiveConstructorExecutor implements ConstructorExecutor { +public class ReflectiveConstructorExecutor implements ConstructorExecutor { private final Constructor ctor; @@ -67,4 +67,8 @@ class ReflectiveConstructorExecutor implements ConstructorExecutor { } } + public Constructor getConstructor() { + return this.ctor; + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java index 138db868ad1..f9955479378 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,12 +31,13 @@ import org.springframework.util.ReflectionUtils; * @author Juergen Hoeller * @since 3.0 */ -class ReflectiveMethodExecutor implements MethodExecutor { +public class ReflectiveMethodExecutor implements MethodExecutor { private final Method method; private final Integer varargsPosition; + private boolean argumentConversionOccurred = false; public ReflectiveMethodExecutor(Method method) { this.method = method; @@ -49,12 +50,20 @@ class ReflectiveMethodExecutor implements MethodExecutor { } } + public Method getMethod() { + return this.method; + } + + public boolean didArgumentConversionOccur() { + return this.argumentConversionOccurred; + } + @Override public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException { try { if (arguments != null) { - ReflectionHelper.convertArguments(context.getTypeConverter(), arguments, this.method, this.varargsPosition); + this.argumentConversionOccurred = ReflectionHelper.convertArguments(context.getTypeConverter(), arguments, this.method, this.varargsPosition); } if (this.method.isVarArgs()) { arguments = ReflectionHelper.setupArgumentsForVarargsInvocation(this.method.getParameterTypes(), arguments); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index bdc8c8f18b1..32d06fb40a7 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -29,15 +29,19 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.asm.MethodVisitor; import org.springframework.core.MethodParameter; import org.springframework.core.convert.Property; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.style.ToStringCreator; import org.springframework.expression.AccessException; +import org.springframework.expression.CompilablePropertyAccessor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ast.PropertyOrFieldReference; +import org.springframework.expression.spel.standard.CodeFlow; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -72,6 +76,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { private final Map typeDescriptorCache = new ConcurrentHashMap(64); + private InvokerPair lastReadInvokerPair; /** * Returns {@code null} which means this is a general purpose accessor. @@ -115,6 +120,10 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { } return false; } + + public Member getLastReadInvokerPair() { + return lastReadInvokerPair.member; + } @Override public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { @@ -132,6 +141,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { CacheKey cacheKey = new CacheKey(type, name, target instanceof Class); InvokerPair invoker = this.readerCache.get(cacheKey); + lastReadInvokerPair = invoker; if (invoker == null || invoker.member instanceof Method) { Method method = (Method) (invoker != null ? invoker.member : null); @@ -144,6 +154,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { Property property = new Property(type, method, null); TypeDescriptor typeDescriptor = new TypeDescriptor(property); invoker = new InvokerPair(method, typeDescriptor); + lastReadInvokerPair = invoker; this.readerCache.put(cacheKey, invoker); } } @@ -165,6 +176,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { field = findField(name, type, target); if (field != null) { invoker = new InvokerPair(field, new TypeDescriptor(field)); + lastReadInvokerPair = invoker; this.readerCache.put(cacheKey, invoker); } } @@ -557,9 +569,9 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { * accessor exists because looking up the appropriate reflective object by class/name * on each read is not cheap. */ - private static class OptimalPropertyAccessor implements PropertyAccessor { + public static class OptimalPropertyAccessor implements CompilablePropertyAccessor { - private final Member member; + public final Member member; private final TypeDescriptor typeDescriptor; @@ -637,6 +649,16 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { } throw new AccessException("Neither getter nor field found for property '" + name + "'"); } + + @Override + public Class getPropertyType() { + if (member instanceof Field) { + return ((Field)member).getType(); + } + else { + return ((Method)member).getReturnType(); + } + } @Override public boolean canWrite(EvaluationContext context, Object target, String name) { @@ -647,6 +669,37 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { public void write(EvaluationContext context, Object target, String name, Object newValue) { throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); } + + @Override + public boolean isCompilable() { + // If non public must continue to use reflection + if (!Modifier.isPublic(member.getModifiers()) || !Modifier.isPublic(member.getDeclaringClass().getModifiers())) { + return false; + } + return true; + } + + @Override + public void generateCode(PropertyOrFieldReference propertyReference, MethodVisitor mv,CodeFlow codeflow) { + boolean isStatic = Modifier.isStatic(member.getModifiers()); + + String descriptor = codeflow.lastDescriptor(); + String memberDeclaringClassSlashedDescriptor = member.getDeclaringClass().getName().replace('.','/'); + if (!isStatic) { + if (descriptor == null) { + codeflow.loadTarget(mv); + } + if (descriptor == null || !memberDeclaringClassSlashedDescriptor.equals(descriptor.substring(1))) { + mv.visitTypeInsn(CHECKCAST, memberDeclaringClassSlashedDescriptor); + } + } + if (member instanceof Field) { + mv.visitFieldInsn(isStatic?GETSTATIC:GETFIELD,memberDeclaringClassSlashedDescriptor,member.getName(),CodeFlow.toJVMDescriptor(((Field) member).getType())); + } else { + mv.visitMethodInsn(isStatic?INVOKESTATIC:INVOKEVIRTUAL, memberDeclaringClassSlashedDescriptor, member.getName(),CodeFlow.createSignatureDescriptor((Method)member),false); + } + } + } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java new file mode 100644 index 00000000000..19e27f46c4e --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -0,0 +1,2603 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression.spel; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.junit.Test; +import org.springframework.asm.MethodVisitor; +import org.springframework.expression.AccessException; +import org.springframework.expression.CompilablePropertyAccessor; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ast.CompoundExpression; +import org.springframework.expression.spel.ast.OpLT; +import org.springframework.expression.spel.ast.PropertyOrFieldReference; +import org.springframework.expression.spel.ast.SpelNodeImpl; +import org.springframework.expression.spel.ast.Ternary; +import org.springframework.expression.spel.standard.CodeFlow; +import org.springframework.expression.spel.standard.SpelCompiler; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.junit.Assert.*; + +/** + * Checks the behaviour of the SpelCompiler. This should cover compilation all compiled node types. + * + * @author Andy Clement + * @since 4.1 + */ +public class SpelCompilationCoverageTests extends AbstractExpressionTests { + + private Expression expression; + private SpelNodeImpl ast; + + /* + * Further TODOs for compilation: + * + * - OpMinus with a single literal operand could be treated as a negative literal. Will save a + * pointless loading of 0 and then a subtract instruction in code gen. + * - allow other accessors/resolvers to participate in compilation and create their own code + * - A TypeReference followed by (what ends up as) a static method invocation can really skip + * code gen for the TypeReference since once that is used to locate the method it is not + * used again. + * - The opEq implementation is quite basic. It will compare numbers of the same type (allowing + * them to be their boxed or unboxed variants) or compare object references. It does not + * compile expressions where numbers are of different types or when objects implement + * Comparable. + * + * Compiled nodes: + * + * TypeReference + * OperatorInstanceOf + * StringLiteral + * NullLiteral + * RealLiteral + * IntLiteral + * LongLiteral + * BooleanLiteral + * FloatLiteral + * OpOr + * OpAnd + * OperatorNot + * Ternary + * Elvis + * VariableReference + * OpLt + * OpLe + * OpGt + * OpGe + * OpEq + * OpNe + * OpPlus + * OpMinus + * OpMultiply + * OpDivide + * MethodReference + * PropertyOrFieldReference + * Indexer + * CompoundExpression + * ConstructorReference + * FunctionReference + * + * Not yet compiled (some may never need to be): + * Assign + * BeanReference + * Identifier + * InlineList + * OpDec + * OpBetween + * OpMatches + * OpPower + * OpInc + * OpModulus + * Projection + * QualifiedId + * Selection + */ + + @Test + public void typeReference() throws Exception { + expression = parse("T(String)"); + assertEquals(String.class,expression.getValue()); + assertCanCompile(expression); + assertEquals(String.class,expression.getValue()); + + expression = parse("T(java.io.IOException)"); + assertEquals(IOException.class,expression.getValue()); + assertCanCompile(expression); + assertEquals(IOException.class,expression.getValue()); + + expression = parse("T(java.io.IOException[])"); + assertEquals(IOException[].class,expression.getValue()); + assertCanCompile(expression); + assertEquals(IOException[].class,expression.getValue()); + + expression = parse("T(int[][])"); + assertEquals(int[][].class,expression.getValue()); + assertCanCompile(expression); + assertEquals(int[][].class,expression.getValue()); + + expression = parse("T(int)"); + assertEquals(Integer.TYPE,expression.getValue()); + assertCanCompile(expression); + assertEquals(Integer.TYPE,expression.getValue()); + + expression = parse("T(byte)"); + assertEquals(Byte.TYPE,expression.getValue()); + assertCanCompile(expression); + assertEquals(Byte.TYPE,expression.getValue()); + + expression = parse("T(char)"); + assertEquals(Character.TYPE,expression.getValue()); + assertCanCompile(expression); + assertEquals(Character.TYPE,expression.getValue()); + + expression = parse("T(short)"); + assertEquals(Short.TYPE,expression.getValue()); + assertCanCompile(expression); + assertEquals(Short.TYPE,expression.getValue()); + + expression = parse("T(long)"); + assertEquals(Long.TYPE,expression.getValue()); + assertCanCompile(expression); + assertEquals(Long.TYPE,expression.getValue()); + + expression = parse("T(float)"); + assertEquals(Float.TYPE,expression.getValue()); + assertCanCompile(expression); + assertEquals(Float.TYPE,expression.getValue()); + + expression = parse("T(double)"); + assertEquals(Double.TYPE,expression.getValue()); + assertCanCompile(expression); + assertEquals(Double.TYPE,expression.getValue()); + + expression = parse("T(boolean)"); + assertEquals(Boolean.TYPE,expression.getValue()); + assertCanCompile(expression); + assertEquals(Boolean.TYPE,expression.getValue()); + + expression = parse("T(Missing)"); + assertGetValueFail(expression); + assertCantCompile(expression); + } + + @SuppressWarnings("unchecked") + @Test + public void operatorInstanceOf() throws Exception { + expression = parse("'xyz' instanceof T(String)"); + assertEquals(true,expression.getValue()); + assertCanCompile(expression); + assertEquals(true,expression.getValue()); + + expression = parse("'xyz' instanceof T(Integer)"); + assertEquals(false,expression.getValue()); + assertCanCompile(expression); + assertEquals(false,expression.getValue()); + + List list = new ArrayList(); + expression = parse("#root instanceof T(java.util.List)"); + assertEquals(true,expression.getValue(list)); + assertCanCompile(expression); + assertEquals(true,expression.getValue(list)); + + List[] arrayOfLists = new List[]{new ArrayList()}; + expression = parse("#root instanceof T(java.util.List[])"); + assertEquals(true,expression.getValue(arrayOfLists)); + assertCanCompile(expression); + assertEquals(true,expression.getValue(arrayOfLists)); + + int[] intArray = new int[]{1,2,3}; + expression = parse("#root instanceof T(int[])"); + assertEquals(true,expression.getValue(intArray)); + assertCanCompile(expression); + assertEquals(true,expression.getValue(intArray)); + + String root = null; + expression = parse("#root instanceof T(Integer)"); + assertEquals(false,expression.getValue(root)); + assertCanCompile(expression); + assertEquals(false,expression.getValue(root)); + + // root still null + expression = parse("#root instanceof T(java.lang.Object)"); + assertEquals(false,expression.getValue(root)); + assertCanCompile(expression); + assertEquals(false,expression.getValue(root)); + + root = "howdy!"; + expression = parse("#root instanceof T(java.lang.Object)"); + assertEquals(true,expression.getValue(root)); + assertCanCompile(expression); + assertEquals(true,expression.getValue(root)); + } + + @Test + public void stringLiteral() throws Exception { + expression = parser.parseExpression("'abcde'"); + assertEquals("abcde",expression.getValue(new TestClass1(),String.class)); + assertCanCompile(expression); + String resultC = expression.getValue(new TestClass1(),String.class); + assertEquals("abcde",resultC); + assertEquals("abcde",expression.getValue(String.class)); + assertEquals("abcde",expression.getValue()); + assertEquals("abcde",expression.getValue(new StandardEvaluationContext())); + expression = parser.parseExpression("\"abcde\""); + assertCanCompile(expression); + assertEquals("abcde",expression.getValue(String.class)); + } + + @Test + public void nullLiteral() throws Exception { + expression = parser.parseExpression("null"); + Object resultI = expression.getValue(new TestClass1(),Object.class); + assertCanCompile(expression); + Object resultC = expression.getValue(new TestClass1(),Object.class); + assertEquals(null,resultI); + assertEquals(null,resultC); + } + + @Test + public void realLiteral() throws Exception { + expression = parser.parseExpression("3.4d"); + double resultI = expression.getValue(new TestClass1(),Double.TYPE); + assertCanCompile(expression); + double resultC = expression.getValue(new TestClass1(),Double.TYPE); + assertEquals(3.4d,resultI,0.1d); + assertEquals(3.4d,resultC,0.1d); + + assertEquals(3.4d,expression.getValue()); + } + + @Test + public void intLiteral() throws Exception { + expression = parser.parseExpression("42"); + int resultI = expression.getValue(new TestClass1(),Integer.TYPE); + assertCanCompile(expression); + int resultC = expression.getValue(new TestClass1(),Integer.TYPE); + assertEquals(42,resultI); + assertEquals(42,resultC); + + expression = parser.parseExpression("T(Integer).valueOf(42)"); + expression.getValue(Integer.class); + assertCanCompile(expression); + assertEquals(new Integer(42),expression.getValue(null,Integer.class)); + + // Code gen is different for -1 .. 6 because there are bytecode instructions specifically for those + // values + + // Not an int literal but an opminus with one operand: +// expression = parser.parseExpression("-1"); +// assertCanCompile(expression); +// assertEquals(-1,expression.getValue()); + expression = parser.parseExpression("0"); + assertCanCompile(expression); + assertEquals(0,expression.getValue()); + expression = parser.parseExpression("2"); + assertCanCompile(expression); + assertEquals(2,expression.getValue()); + expression = parser.parseExpression("7"); + assertCanCompile(expression); + assertEquals(7,expression.getValue()); + } + + @Test + public void longLiteral() throws Exception { + expression = parser.parseExpression("99L"); + long resultI = expression.getValue(new TestClass1(),Long.TYPE); + assertCanCompile(expression); + long resultC = expression.getValue(new TestClass1(),Long.TYPE); + assertEquals(99L,resultI); + assertEquals(99L,resultC); + } + + @Test + public void booleanLiteral() throws Exception { + expression = parser.parseExpression("true"); + boolean resultI = expression.getValue(1,Boolean.TYPE); + assertEquals(true,resultI); + assertTrue(SpelCompiler.compile(expression)); + boolean resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(true,resultC); + + expression = parser.parseExpression("false"); + resultI = expression.getValue(1,Boolean.TYPE); + assertEquals(false,resultI); + assertTrue(SpelCompiler.compile(expression)); + resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(false,resultC); + } + + @Test + public void floatLiteral() throws Exception { + expression = parser.parseExpression("3.4f"); + float resultI = expression.getValue(new TestClass1(),Float.TYPE); + assertCanCompile(expression); + float resultC = expression.getValue(new TestClass1(),Float.TYPE); + assertEquals(3.4f,resultI,0.1f); + assertEquals(3.4f,resultC,0.1f); + + assertEquals(3.4f,expression.getValue()); + } + + @Test + public void opOr() throws Exception { + Expression expression = parser.parseExpression("false or false"); + boolean resultI = expression.getValue(1,Boolean.TYPE); + SpelCompiler.compile(expression); + boolean resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(false,resultI); + assertEquals(false,resultC); + + expression = parser.parseExpression("false or true"); + resultI = expression.getValue(1,Boolean.TYPE); + assertCanCompile(expression); + resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(true,resultI); + assertEquals(true,resultC); + + expression = parser.parseExpression("true or false"); + resultI = expression.getValue(1,Boolean.TYPE); + assertCanCompile(expression); + resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(true,resultI); + assertEquals(true,resultC); + + expression = parser.parseExpression("true or true"); + resultI = expression.getValue(1,Boolean.TYPE); + assertCanCompile(expression); + resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(true,resultI); + assertEquals(true,resultC); + + TestClass4 tc = new TestClass4(); + expression = parser.parseExpression("getfalse() or gettrue()"); + resultI = expression.getValue(tc,Boolean.TYPE); + assertCanCompile(expression); + resultC = expression.getValue(tc,Boolean.TYPE); + assertEquals(true,resultI); + assertEquals(true,resultC); + + // Can't compile this as we aren't going down the getfalse() branch in our evaluation + expression = parser.parseExpression("gettrue() or getfalse()"); + resultI = expression.getValue(tc,Boolean.TYPE); + assertCantCompile(expression); + + expression = parser.parseExpression("getA() or getB()"); + tc.a = true; + tc.b = true; + resultI = expression.getValue(tc,Boolean.TYPE); + assertCantCompile(expression); // Haven't yet been into second branch + tc.a = false; + tc.b = true; + resultI = expression.getValue(tc,Boolean.TYPE); + assertCanCompile(expression); // Now been down both + assertTrue(resultI); + + boolean b = false; + expression = parse("#root or #root"); + Object resultI2 = expression.getValue(b); + assertCanCompile(expression); + assertFalse((Boolean)resultI2); + assertFalse((Boolean)expression.getValue(b)); + } + + @Test + public void opAnd() throws Exception { + Expression expression = parser.parseExpression("false and false"); + boolean resultI = expression.getValue(1,Boolean.TYPE); + SpelCompiler.compile(expression); + boolean resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(false,resultI); + assertEquals(false,resultC); + + expression = parser.parseExpression("false and true"); + resultI = expression.getValue(1,Boolean.TYPE); + SpelCompiler.compile(expression); + resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(false,resultI); + assertEquals(false,resultC); + + expression = parser.parseExpression("true and false"); + resultI = expression.getValue(1,Boolean.TYPE); + SpelCompiler.compile(expression); + resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(false,resultI); + assertEquals(false,resultC); + + expression = parser.parseExpression("true and true"); + resultI = expression.getValue(1,Boolean.TYPE); + SpelCompiler.compile(expression); + resultC = expression.getValue(1,Boolean.TYPE); + assertEquals(true,resultI); + assertEquals(true,resultC); + + TestClass4 tc = new TestClass4(); + + // Can't compile this as we aren't going down the gettrue() branch in our evaluation + expression = parser.parseExpression("getfalse() and gettrue()"); + resultI = expression.getValue(tc,Boolean.TYPE); + assertCantCompile(expression); + + expression = parser.parseExpression("getA() and getB()"); + tc.a = false; + tc.b = false; + resultI = expression.getValue(tc,Boolean.TYPE); + assertCantCompile(expression); // Haven't yet been into second branch + tc.a = true; + tc.b = false; + resultI = expression.getValue(tc,Boolean.TYPE); + assertCanCompile(expression); // Now been down both + assertFalse(resultI); + tc.a = true; + tc.b = true; + resultI = expression.getValue(tc,Boolean.TYPE); + assertTrue(resultI); + + boolean b = true; + expression = parse("#root and #root"); + Object resultI2 = expression.getValue(b); + assertCanCompile(expression); + assertTrue((Boolean)resultI2); + assertTrue((Boolean)expression.getValue(b)); + } + + @Test + public void operatorNot() throws Exception { + expression = parse("!true"); + assertEquals(false,expression.getValue()); + assertCanCompile(expression); + assertEquals(false,expression.getValue()); + + expression = parse("!false"); + assertEquals(true,expression.getValue()); + assertCanCompile(expression); + assertEquals(true,expression.getValue()); + + boolean b = true; + expression = parse("!#root"); + assertEquals(false,expression.getValue(b)); + assertCanCompile(expression); + assertEquals(false,expression.getValue(b)); + + b = false; + expression = parse("!#root"); + assertEquals(true,expression.getValue(b)); + assertCanCompile(expression); + assertEquals(true,expression.getValue(b)); + } + + @Test + public void ternary() throws Exception { + Expression expression = parser.parseExpression("true?'a':'b'"); + String resultI = expression.getValue(String.class); + assertCanCompile(expression); + String resultC = expression.getValue(String.class); + assertEquals("a",resultI); + assertEquals("a",resultC); + + expression = parser.parseExpression("false?'a':'b'"); + resultI = expression.getValue(String.class); + assertCanCompile(expression); + resultC = expression.getValue(String.class); + assertEquals("b",resultI); + assertEquals("b",resultC); + + expression = parser.parseExpression("false?1:'b'"); + // All literals so we can do this straight away + assertCanCompile(expression); + assertEquals("b",expression.getValue()); + + boolean root = true; + expression = parser.parseExpression("(#root and true)?T(Integer).valueOf(1):T(Long).valueOf(3L)"); + assertEquals(1,expression.getValue(root)); + assertCantCompile(expression); // Have not gone down false branch + root = false; + assertEquals(3L,expression.getValue(root)); + assertCanCompile(expression); + assertEquals(3L,expression.getValue(root)); + root = true; + assertEquals(1,expression.getValue(root)); + } + + @Test + public void elvis() throws Exception { + Expression expression = parser.parseExpression("'a'?:'b'"); + String resultI = expression.getValue(String.class); + assertCanCompile(expression); + String resultC = expression.getValue(String.class); + assertEquals("a",resultI); + assertEquals("a",resultC); + + expression = parser.parseExpression("null?:'a'"); + resultI = expression.getValue(String.class); + assertCanCompile(expression); + resultC = expression.getValue(String.class); + assertEquals("a",resultI); + assertEquals("a",resultC); + + String s = "abc"; + expression = parser.parseExpression("#root?:'b'"); + assertCantCompile(expression); + resultI = expression.getValue(s,String.class); + assertEquals("abc",resultI); + assertCanCompile(expression); + } + + @Test + public void variableReference_root() throws Exception { + String s = "hello"; + Expression expression = parser.parseExpression("#root"); + String resultI = expression.getValue(s,String.class); + assertCanCompile(expression); + String resultC = expression.getValue(s,String.class); + assertEquals(s,resultI); + assertEquals(s,resultC); + + expression = parser.parseExpression("#root"); + int i = (Integer)expression.getValue(42); + assertEquals(42,i); + assertCanCompile(expression); + i = (Integer)expression.getValue(42); + assertEquals(42,i); + } + + public static String concat(String a, String b) { + return a+b; + } + + @Test + public void functionReference() throws Exception { + EvaluationContext ctx = new StandardEvaluationContext(); + Method m = this.getClass().getDeclaredMethod("concat",String.class,String.class); + ctx.setVariable("concat",m); + + expression = parser.parseExpression("#concat('a','b')"); + assertEquals("ab",expression.getValue(ctx)); + assertCanCompile(expression); + assertEquals("ab",expression.getValue(ctx)); + + expression = parser.parseExpression("#concat(#concat('a','b'),'c').charAt(1)"); + assertEquals('b',expression.getValue(ctx)); + assertCanCompile(expression); + assertEquals('b',expression.getValue(ctx)); + + expression = parser.parseExpression("#concat(#a,#b)"); + ctx.setVariable("a", "foo"); + ctx.setVariable("b", "bar"); + assertEquals("foobar",expression.getValue(ctx)); + assertCanCompile(expression); + assertEquals("foobar",expression.getValue(ctx)); + ctx.setVariable("b", "boo"); + assertEquals("fooboo",expression.getValue(ctx)); + } + + @Test + public void variableReference_userDefined() throws Exception { + EvaluationContext ctx = new StandardEvaluationContext(); + ctx.setVariable("target", "abc"); + expression = parser.parseExpression("#target"); + assertEquals("abc",expression.getValue(ctx)); + assertCanCompile(expression); + assertEquals("abc",expression.getValue(ctx)); + ctx.setVariable("target", "123"); + assertEquals("123",expression.getValue(ctx)); + ctx.setVariable("target", 42); + try { + assertEquals(42,expression.getValue(ctx)); + fail(); + } catch (SpelEvaluationException see) { + assertTrue(see.getCause() instanceof ClassCastException); + } + + ctx.setVariable("target", "abc"); + expression = parser.parseExpression("#target.charAt(0)"); + assertEquals('a',expression.getValue(ctx)); + assertCanCompile(expression); + assertEquals('a',expression.getValue(ctx)); + ctx.setVariable("target", "1"); + assertEquals('1',expression.getValue(ctx)); + ctx.setVariable("target", 42); + try { + assertEquals('4',expression.getValue(ctx)); + fail(); + } catch (SpelEvaluationException see) { + assertTrue(see.getCause() instanceof ClassCastException); + } + } + + @Test + public void opLt() throws Exception { + expression = parse("3.0d < 4.0d"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("3446.0d < 1123.0d"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("3 < 1"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("2 < 4"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("3.0f < 1.0f"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("1.0f < 5.0f"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("30L < 30L"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("15L < 20L"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + // Differing types of number, not yet supported + expression = parse("1 < 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) < 4"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Integer).valueOf(3) < T(Integer).valueOf(3)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("5 < T(Integer).valueOf(3)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + } + + @Test + public void opLe() throws Exception { + expression = parse("3.0d <= 4.0d"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("3446.0d <= 1123.0d"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("3446.0d <= 3446.0d"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("3 <= 1"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("2 <= 4"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("3 <= 3"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("3.0f <= 1.0f"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("1.0f <= 5.0f"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("2.0f <= 2.0f"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("30L <= 30L"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("15L <= 20L"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + // Differing types of number, not yet supported + expression = parse("1 <= 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) <= 4"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Integer).valueOf(3) <= T(Integer).valueOf(3)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("5 <= T(Integer).valueOf(3)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + } + + + @Test + public void opGt() throws Exception { + expression = parse("3.0d > 4.0d"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("3446.0d > 1123.0d"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("3 > 1"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("2 > 4"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("3.0f > 1.0f"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("1.0f > 5.0f"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("30L > 30L"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("15L > 20L"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + // Differing types of number, not yet supported + expression = parse("1 > 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) > 4"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Integer).valueOf(3) > T(Integer).valueOf(3)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("5 > T(Integer).valueOf(3)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + } + + @Test + public void opGe() throws Exception { + expression = parse("3.0d >= 4.0d"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("3446.0d >= 1123.0d"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("3446.0d >= 3446.0d"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("3 >= 1"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("2 >= 4"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("3 >= 3"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("3.0f >= 1.0f"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("1.0f >= 5.0f"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("3.0f >= 3.0f"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("40L >= 30L"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("15L >= 20L"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("30L >= 30L"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + // Differing types of number, not yet supported + expression = parse("1 >= 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) >= 4"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Integer).valueOf(3) >= T(Integer).valueOf(3)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("5 >= T(Integer).valueOf(3)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + } + + @Test + public void opEq() throws Exception { + + TestClass7 tc7 = new TestClass7(); + expression = parse("property == 'UK'"); + assertTrue((Boolean)expression.getValue(tc7)); + TestClass7.property = null; + assertFalse((Boolean)expression.getValue(tc7)); + assertCanCompile(expression); + TestClass7.reset(); + assertTrue((Boolean)expression.getValue(tc7)); + TestClass7.property = "UK"; + assertTrue((Boolean)expression.getValue(tc7)); + TestClass7.reset(); + TestClass7.property = null; + assertFalse((Boolean)expression.getValue(tc7)); + expression = parse("property == null"); + assertTrue((Boolean)expression.getValue(tc7)); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue(tc7)); + + + expression = parse("3.0d == 4.0d"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("3446.0d == 3446.0d"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("3 == 1"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("3 == 3"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("3.0f == 1.0f"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("2.0f == 2.0f"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("30L == 30L"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("15L == 20L"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + // number types are not the same + expression = parse("1 == 3.0d"); + assertCantCompile(expression); + + Double d = 3.0d; + expression = parse("#root==3.0d"); + assertTrue((Boolean)expression.getValue(d)); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue(d)); + + Integer i = 3; + expression = parse("#root==3"); + assertTrue((Boolean)expression.getValue(i)); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue(i)); + + Float f = 3.0f; + expression = parse("#root==3.0f"); + assertTrue((Boolean)expression.getValue(f)); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue(f)); + + long l = 300l; + expression = parse("#root==300l"); + assertTrue((Boolean)expression.getValue(l)); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue(l)); + + boolean b = true; + expression = parse("#root==true"); + assertTrue((Boolean)expression.getValue(b)); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue(b)); + + expression = parse("T(Integer).valueOf(3) == 4"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Integer).valueOf(3) == T(Integer).valueOf(3)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("5 == T(Integer).valueOf(3)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Float).valueOf(3.0f) == 4.0f"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Float).valueOf(3.0f) == T(Float).valueOf(3.0f)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("5.0f == T(Float).valueOf(3.0f)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Long).valueOf(3L) == 4L"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Long).valueOf(3L) == T(Long).valueOf(3L)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("5L == T(Long).valueOf(3L)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Double).valueOf(3.0d) == 4.0d"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Double).valueOf(3.0d) == T(Double).valueOf(3.0d)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("5.0d == T(Double).valueOf(3.0d)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("false == true"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Boolean).valueOf('true') == T(Boolean).valueOf('true')"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Boolean).valueOf('true') == true"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("false == T(Boolean).valueOf('false')"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + } + + @Test + public void opNe() throws Exception { + expression = parse("3.0d != 4.0d"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("3446.0d != 3446.0d"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("3 != 1"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("3 != 3"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("3.0f != 1.0f"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + expression = parse("2.0f != 2.0f"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("30L != 30L"); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + expression = parse("15L != 20L"); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + // not compatible number types + expression = parse("1 != 3.0d"); + assertCantCompile(expression); + + expression = parse("T(Integer).valueOf(3) != 4"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Integer).valueOf(3) != T(Integer).valueOf(3)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("5 != T(Integer).valueOf(3)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Float).valueOf(3.0f) != 4.0f"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Float).valueOf(3.0f) != T(Float).valueOf(3.0f)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("5.0f != T(Float).valueOf(3.0f)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Long).valueOf(3L) != 4L"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Long).valueOf(3L) != T(Long).valueOf(3L)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("5L != T(Long).valueOf(3L)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Double).valueOf(3.0d) == 4.0d"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Double).valueOf(3.0d) == T(Double).valueOf(3.0d)"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("5.0d == T(Double).valueOf(3.0d)"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("false == true"); + assertFalse((Boolean)expression.getValue()); + assertCanCompile(expression); + assertFalse((Boolean)expression.getValue()); + + expression = parse("T(Boolean).valueOf('true') == T(Boolean).valueOf('true')"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("T(Boolean).valueOf('true') == true"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + + expression = parse("false == T(Boolean).valueOf('false')"); + assertTrue((Boolean)expression.getValue()); + assertCanCompile(expression); + assertTrue((Boolean)expression.getValue()); + } + + @Test + public void opPlus() throws Exception { + expression = parse("2+2"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(4,expression.getValue()); + + expression = parse("2L+2L"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(4L,expression.getValue()); + + expression = parse("2.0f+2.0f"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(4.0f,expression.getValue()); + + expression = parse("3.0d+4.0d"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(7.0d,expression.getValue()); + + expression = parse("+1"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(1,expression.getValue()); + + expression = parse("+1L"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(1L,expression.getValue()); + + expression = parse("+1.5f"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(1.5f,expression.getValue()); + + expression = parse("+2.5d"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(2.5d,expression.getValue()); + + expression = parse("+T(Double).valueOf(2.5d)"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(2.5d,expression.getValue()); + + expression = parse("T(Integer).valueOf(2)+6"); + assertEquals(8,expression.getValue()); + assertCanCompile(expression); + assertEquals(8,expression.getValue()); + + expression = parse("T(Integer).valueOf(1)+T(Integer).valueOf(3)"); + assertEquals(4,expression.getValue()); + assertCanCompile(expression); + assertEquals(4,expression.getValue()); + + expression = parse("1+T(Integer).valueOf(3)"); + assertEquals(4,expression.getValue()); + assertCanCompile(expression); + assertEquals(4,expression.getValue()); + + expression = parse("T(Float).valueOf(2.0f)+6"); + assertEquals(8.0f,expression.getValue()); + assertCantCompile(expression); + + expression = parse("T(Float).valueOf(2.0f)+T(Float).valueOf(3.0f)"); + assertEquals(5.0f,expression.getValue()); + assertCanCompile(expression); + assertEquals(5.0f,expression.getValue()); + + expression = parse("3L+T(Long).valueOf(4L)"); + assertEquals(7L,expression.getValue()); + assertCanCompile(expression); + assertEquals(7L,expression.getValue()); + + expression = parse("T(Long).valueOf(2L)+6"); + assertEquals(8L,expression.getValue()); + assertCantCompile(expression); + + expression = parse("T(Long).valueOf(2L)+T(Long).valueOf(3L)"); + assertEquals(5L,expression.getValue()); + assertCanCompile(expression); + assertEquals(5L,expression.getValue()); + + expression = parse("1L+T(Long).valueOf(2L)"); + assertEquals(3L,expression.getValue()); + assertCanCompile(expression); + assertEquals(3L,expression.getValue()); + } + + @Test + public void opMinus() throws Exception { + expression = parse("2-2"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(0,expression.getValue()); + + expression = parse("4L-2L"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(2L,expression.getValue()); + + expression = parse("4.0f-2.0f"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(2.0f,expression.getValue()); + + expression = parse("3.0d-4.0d"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(-1.0d,expression.getValue()); + + expression = parse("-1"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(-1,expression.getValue()); + + expression = parse("-1L"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(-1L,expression.getValue()); + + expression = parse("-1.5f"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(-1.5f,expression.getValue()); + + expression = parse("-2.5d"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(-2.5d,expression.getValue()); + + expression = parse("T(Integer).valueOf(2)-6"); + assertEquals(-4,expression.getValue()); + assertCanCompile(expression); + assertEquals(-4,expression.getValue()); + + expression = parse("T(Integer).valueOf(1)-T(Integer).valueOf(3)"); + assertEquals(-2,expression.getValue()); + assertCanCompile(expression); + assertEquals(-2,expression.getValue()); + + expression = parse("4-T(Integer).valueOf(3)"); + assertEquals(1,expression.getValue()); + assertCanCompile(expression); + assertEquals(1,expression.getValue()); + + expression = parse("T(Float).valueOf(2.0f)-6"); + assertEquals(-4.0f,expression.getValue()); + assertCantCompile(expression); + + expression = parse("T(Float).valueOf(8.0f)-T(Float).valueOf(3.0f)"); + assertEquals(5.0f,expression.getValue()); + assertCanCompile(expression); + assertEquals(5.0f,expression.getValue()); + + expression = parse("11L-T(Long).valueOf(4L)"); + assertEquals(7L,expression.getValue()); + assertCanCompile(expression); + assertEquals(7L,expression.getValue()); + + expression = parse("T(Long).valueOf(9L)-6"); + assertEquals(3L,expression.getValue()); + assertCantCompile(expression); + + expression = parse("T(Long).valueOf(4L)-T(Long).valueOf(3L)"); + assertEquals(1L,expression.getValue()); + assertCanCompile(expression); + assertEquals(1L,expression.getValue()); + + expression = parse("8L-T(Long).valueOf(2L)"); + assertEquals(6L,expression.getValue()); + assertCanCompile(expression); + assertEquals(6L,expression.getValue()); + } + + + @Test + public void opMultiply() throws Exception { + expression = parse("2*2"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(4,expression.getValue()); + + expression = parse("2L*2L"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(4L,expression.getValue()); + + expression = parse("2.0f*2.0f"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(4.0f,expression.getValue()); + + expression = parse("3.0d*4.0d"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(12.0d,expression.getValue()); + + expression = parse("T(Float).valueOf(2.0f)*6"); + assertEquals(12.0f,expression.getValue()); + assertCantCompile(expression); + + expression = parse("T(Float).valueOf(8.0f)*T(Float).valueOf(3.0f)"); + assertEquals(24.0f,expression.getValue()); + assertCanCompile(expression); + assertEquals(24.0f,expression.getValue()); + + expression = parse("11L*T(Long).valueOf(4L)"); + assertEquals(44L,expression.getValue()); + assertCanCompile(expression); + assertEquals(44L,expression.getValue()); + + expression = parse("T(Long).valueOf(9L)*6"); + assertEquals(54L,expression.getValue()); + assertCantCompile(expression); + + expression = parse("T(Long).valueOf(4L)*T(Long).valueOf(3L)"); + assertEquals(12L,expression.getValue()); + assertCanCompile(expression); + assertEquals(12L,expression.getValue()); + + expression = parse("8L*T(Long).valueOf(2L)"); + assertEquals(16L,expression.getValue()); + assertCanCompile(expression); + assertEquals(16L,expression.getValue()); + + expression = parse("T(Float).valueOf(8.0f)*-T(Float).valueOf(3.0f)"); + assertEquals(-24.0f,expression.getValue()); + assertCanCompile(expression); + assertEquals(-24.0f,expression.getValue()); + } + + @Test + public void opDivide() throws Exception { + expression = parse("2/2"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(1,expression.getValue()); + + expression = parse("2L/2L"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(1L,expression.getValue()); + + expression = parse("2.0f/2.0f"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(1.0f,expression.getValue()); + + expression = parse("3.0d/4.0d"); + expression.getValue(); + assertCanCompile(expression); + assertEquals(0.75d,expression.getValue()); + + expression = parse("T(Float).valueOf(6.0f)/2"); + assertEquals(3.0f,expression.getValue()); + assertCantCompile(expression); + + expression = parse("T(Float).valueOf(8.0f)/T(Float).valueOf(2.0f)"); + assertEquals(4.0f,expression.getValue()); + assertCanCompile(expression); + assertEquals(4.0f,expression.getValue()); + + expression = parse("12L/T(Long).valueOf(4L)"); + assertEquals(3L,expression.getValue()); + assertCanCompile(expression); + assertEquals(3L,expression.getValue()); + + expression = parse("T(Long).valueOf(44L)/11"); + assertEquals(4L,expression.getValue()); + assertCantCompile(expression); + + expression = parse("T(Long).valueOf(4L)/T(Long).valueOf(2L)"); + assertEquals(2L,expression.getValue()); + assertCanCompile(expression); + assertEquals(2L,expression.getValue()); + + expression = parse("8L/T(Long).valueOf(2L)"); + assertEquals(4L,expression.getValue()); + assertCanCompile(expression); + assertEquals(4L,expression.getValue()); + + expression = parse("T(Float).valueOf(8.0f)/-T(Float).valueOf(4.0f)"); + assertEquals(-2.0f,expression.getValue()); + assertCanCompile(expression); + assertEquals(-2.0f,expression.getValue()); + } + + + @Test + public void constructorReference() throws Exception { + // simple ctor + expression = parser.parseExpression("new String('123')"); + assertEquals("123",expression.getValue()); + assertCanCompile(expression); + assertEquals("123",expression.getValue()); + + String testclass8 = "org.springframework.expression.spel.SpelCompilationCoverageTests$TestClass8"; + // multi arg ctor that includes primitives + expression = parser.parseExpression("new "+testclass8+"(42,'123',4.0d,true)"); + assertEquals(testclass8,expression.getValue().getClass().getName()); + assertCanCompile(expression); + Object o = expression.getValue(); + assertEquals(testclass8,o.getClass().getName()); + TestClass8 tc8 = (TestClass8)o; + assertEquals(42,tc8.i); + assertEquals("123",tc8.s); + assertEquals(4.0d,tc8.d,0.5d); + assertEquals(true,tc8.z); + + // no-arg ctor + expression = parser.parseExpression("new "+testclass8+"()"); + assertEquals(testclass8,expression.getValue().getClass().getName()); + assertCanCompile(expression); + o = expression.getValue(); + assertEquals(testclass8,o.getClass().getName()); + + // pass primitive to reference type ctor + expression = parser.parseExpression("new "+testclass8+"(42)"); + assertEquals(testclass8,expression.getValue().getClass().getName()); + assertCanCompile(expression); + o = expression.getValue(); + assertEquals(testclass8,o.getClass().getName()); + tc8 = (TestClass8)o; + assertEquals(42,tc8.i); + + // private class, can't compile it + String testclass9 = "org.springframework.expression.spel.SpelCompilationCoverageTests$TestClass9"; + expression = parser.parseExpression("new "+testclass9+"(42)"); + assertEquals(testclass9,expression.getValue().getClass().getName()); + assertCantCompile(expression); + } + + @Test + public void methodReference() throws Exception { + TestClass5 tc = new TestClass5(); + + // non-static method, no args, void return + expression = parser.parseExpression("one()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertEquals(1,tc.i); + tc.reset(); + + // static method, no args, void return + expression = parser.parseExpression("two()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertEquals(1,TestClass5._i); + tc.reset(); + + // non-static method, reference type return + expression = parser.parseExpression("three()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + assertEquals("hello",expression.getValue(tc)); + tc.reset(); + + // non-static method, primitive type return + expression = parser.parseExpression("four()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + assertEquals(3277700L,expression.getValue(tc)); + tc.reset(); + + // static method, reference type return + expression = parser.parseExpression("five()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + assertEquals("hello",expression.getValue(tc)); + tc.reset(); + + // static method, primitive type return + expression = parser.parseExpression("six()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + assertEquals(3277700L,expression.getValue(tc)); + tc.reset(); + + // non-static method, one parameter of reference type + expression = parser.parseExpression("seven(\"foo\")"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertEquals("foo",tc.s); + tc.reset(); + + // static method, one parameter of reference type + expression = parser.parseExpression("eight(\"bar\")"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertEquals("bar",TestClass5._s); + tc.reset(); + + // non-static method, one parameter of primitive type + expression = parser.parseExpression("nine(231)"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertEquals(231,tc.i); + tc.reset(); + + // static method, one parameter of primitive type + expression = parser.parseExpression("ten(111)"); + assertCantCompile(expression); + expression.getValue(tc); + assertCanCompile(expression); + tc.reset(); + expression.getValue(tc); + assertEquals(111,TestClass5._i); + tc.reset(); + + // non-static method, varargs with reference type + expression = parser.parseExpression("eleven(\"a\",\"b\",\"c\")"); + assertCantCompile(expression); + expression.getValue(tc); + assertCantCompile(expression); // Varargs is not yet supported + + expression = parser.parseExpression("eleven()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCantCompile(expression); // Varargs is not yet supported + + // static method, varargs with primitive type + expression = parser.parseExpression("twelve(1,2,3)"); + assertCantCompile(expression); + expression.getValue(tc); + assertCantCompile(expression); // Varargs is not yet supported + + expression = parser.parseExpression("twelve()"); + assertCantCompile(expression); + expression.getValue(tc); + assertCantCompile(expression); // Varargs is not yet supported + + // method that gets type converted parameters + + // Converting from an int to a string + expression = parser.parseExpression("seven(123)"); + assertCantCompile(expression); + expression.getValue(tc); + assertEquals("123",tc.s); + assertCantCompile(expression); // Uncompilable as argument conversion is occurring + + Expression expression = parser.parseExpression("'abcd'.substring(index1,index2)"); + String resultI = expression.getValue(new TestClass1(),String.class); + assertCanCompile(expression); + String resultC = expression.getValue(new TestClass1(),String.class); + assertEquals("bc",resultI); + assertEquals("bc",resultC); + + // Converting from an int to a Number + expression = parser.parseExpression("takeNumber(123)"); + assertCantCompile(expression); + expression.getValue(tc); + assertEquals("123",tc.s); + tc.reset(); + assertCanCompile(expression); // The generated code should include boxing of the int to a Number + expression.getValue(tc); + assertEquals("123",tc.s); + + // Passing a subtype + expression = parser.parseExpression("takeNumber(T(Integer).valueOf(42))"); + assertCantCompile(expression); + expression.getValue(tc); + assertEquals("42",tc.s); + tc.reset(); + assertCanCompile(expression); // The generated code should include boxing of the int to a Number + expression.getValue(tc); + assertEquals("42",tc.s); + + // Passing a subtype + expression = parser.parseExpression("takeString(T(Integer).valueOf(42))"); + assertCantCompile(expression); + expression.getValue(tc); + assertEquals("42",tc.s); + tc.reset(); + assertCantCompile(expression); // method takes a string and we are passing an Integer + } + + + @Test + public void errorHandling() throws Exception { + TestClass5 tc = new TestClass5(); + + // changing target + + // from primitive array to reference type array + int[] is = new int[]{1,2,3}; + String[] strings = new String[]{"a","b","c"}; + expression = parser.parseExpression("[1]"); + assertEquals(2,expression.getValue(is)); + assertCanCompile(expression); + assertEquals(2,expression.getValue(is)); + + try { + assertEquals(2,expression.getValue(strings)); + fail(); + } catch (SpelEvaluationException see) { + assertTrue(see.getCause() instanceof ClassCastException); + } + SpelCompiler.revertToInterpreted(expression); + assertEquals("b",expression.getValue(strings)); + assertCanCompile(expression); + assertEquals("b",expression.getValue(strings)); + + + tc.field = "foo"; + expression = parser.parseExpression("seven(field)"); + assertCantCompile(expression); + expression.getValue(tc); + assertEquals("foo",tc.s); + assertCanCompile(expression); + tc.reset(); + tc.field="bar"; + expression.getValue(tc); + + // method with changing parameter types (change reference type) + tc.obj = "foo"; + expression = parser.parseExpression("seven(obj)"); + assertCantCompile(expression); + expression.getValue(tc); + assertEquals("foo",tc.s); + assertCanCompile(expression); + tc.reset(); + tc.obj=new Integer(42); + try { + expression.getValue(tc); + fail(); + } catch (SpelEvaluationException see) { + assertTrue(see.getCause() instanceof ClassCastException); + } + + + // method with changing target + expression = parser.parseExpression("#root.charAt(0)"); + assertEquals('a',expression.getValue("abc")); + assertCanCompile(expression); + try { + expression.getValue(new Integer(42)); + fail(); + } catch (SpelEvaluationException see) { + // java.lang.Integer cannot be cast to java.lang.String + assertTrue(see.getCause() instanceof ClassCastException); + } + } + + @Test + public void methodReference_staticMethod() throws Exception { + Expression expression = parser.parseExpression("T(Integer).valueOf(42)"); + int resultI = expression.getValue(new TestClass1(),Integer.TYPE); + assertCanCompile(expression); + int resultC = expression.getValue(new TestClass1(),Integer.TYPE); + assertEquals(42,resultI); + assertEquals(42,resultC); + } + + @Test + public void methodReference_literalArguments_int() throws Exception { + Expression expression = parser.parseExpression("'abcd'.substring(1,3)"); + String resultI = expression.getValue(new TestClass1(),String.class); + assertCanCompile(expression); + String resultC = expression.getValue(new TestClass1(),String.class); + assertEquals("bc",resultI); + assertEquals("bc",resultC); + } + + @Test + public void methodReference_simpleInstanceMethodNoArg() throws Exception { + Expression expression = parser.parseExpression("toString()"); + String resultI = expression.getValue(42,String.class); + assertCanCompile(expression); + String resultC = expression.getValue(42,String.class); + assertEquals("42",resultI); + assertEquals("42",resultC); + } + + @Test + public void methodReference_simpleInstanceMethodNoArgReturnPrimitive() throws Exception { + expression = parser.parseExpression("intValue()"); + int resultI = expression.getValue(new Integer(42),Integer.TYPE); + assertEquals(42,resultI); + assertCanCompile(expression); + int resultC = expression.getValue(new Integer(42),Integer.TYPE); + assertEquals(42,resultC); + } + + @Test + public void methodReference_simpleInstanceMethodOneArgReturnPrimitive1() throws Exception { + Expression expression = parser.parseExpression("indexOf('b')"); + int resultI = expression.getValue("abc",Integer.TYPE); + assertCanCompile(expression); + int resultC = expression.getValue("abc",Integer.TYPE); + assertEquals(1,resultI); + assertEquals(1,resultC); + } + + @Test + public void methodReference_simpleInstanceMethodOneArgReturnPrimitive2() throws Exception { + expression = parser.parseExpression("charAt(2)"); + char resultI = expression.getValue("abc",Character.TYPE); + assertEquals('c',resultI); + assertCanCompile(expression); + char resultC = expression.getValue("abc",Character.TYPE); + assertEquals('c',resultC); + } + + + @Test + public void compoundExpression() throws Exception { + Payload payload = new Payload(); + expression = parser.parseExpression("DR[0]"); + assertEquals("instanceof Two",expression.getValue(payload).toString()); + assertCanCompile(expression); + assertEquals("instanceof Two",expression.getValue(payload).toString()); + ast = getAst(); + assertEquals("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Two",ast.getExitDescriptor()); + + expression = parser.parseExpression("holder.three"); + assertEquals("org.springframework.expression.spel.SpelCompilationCoverageTests$Three",expression.getValue(payload).getClass().getName()); + assertCanCompile(expression); + assertEquals("org.springframework.expression.spel.SpelCompilationCoverageTests$Three",expression.getValue(payload).getClass().getName()); + ast = getAst(); + assertEquals("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Three",ast.getExitDescriptor()); + + expression = parser.parseExpression("DR[0]"); + assertEquals("org.springframework.expression.spel.SpelCompilationCoverageTests$Two",expression.getValue(payload).getClass().getName()); + assertCanCompile(expression); + assertEquals("org.springframework.expression.spel.SpelCompilationCoverageTests$Two",expression.getValue(payload).getClass().getName()); + assertEquals("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Two",getAst().getExitDescriptor()); + + expression = parser.parseExpression("DR[0].three"); + assertEquals("org.springframework.expression.spel.SpelCompilationCoverageTests$Three",expression.getValue(payload).getClass().getName()); + assertCanCompile(expression); + assertEquals("org.springframework.expression.spel.SpelCompilationCoverageTests$Three",expression.getValue(payload).getClass().getName()); + ast = getAst(); + assertEquals("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Three",ast.getExitDescriptor()); + + expression = parser.parseExpression("DR[0].three.four"); + assertEquals(0.04d,expression.getValue(payload)); + assertCanCompile(expression); + assertEquals(0.04d,expression.getValue(payload)); + assertEquals("D",getAst().getExitDescriptor()); + } + + + @Test + public void mixingItUp_indexerOpEqTernary() throws Exception { + Map m = new HashMap(); + m.put("andy","778"); + + expression = parse("['andy']==null?1:2"); + System.out.println(expression.getValue(m)); + assertCanCompile(expression); + assertEquals(2,expression.getValue(m)); + m.remove("andy"); + assertEquals(1,expression.getValue(m)); + } + + @Test + public void propertyReference() throws Exception { + TestClass6 tc = new TestClass6(); + + // non static field + expression = parser.parseExpression("orange"); + assertCantCompile(expression); + assertEquals("value1",expression.getValue(tc)); + assertCanCompile(expression); + assertEquals("value1",expression.getValue(tc)); + + // static field + expression = parser.parseExpression("apple"); + assertCantCompile(expression); + assertEquals("value2",expression.getValue(tc)); + assertCanCompile(expression); + assertEquals("value2",expression.getValue(tc)); + + // non static getter + expression = parser.parseExpression("banana"); + assertCantCompile(expression); + assertEquals("value3",expression.getValue(tc)); + assertCanCompile(expression); + assertEquals("value3",expression.getValue(tc)); + + // static getter + expression = parser.parseExpression("plum"); + assertCantCompile(expression); + assertEquals("value4",expression.getValue(tc)); + assertCanCompile(expression); + assertEquals("value4",expression.getValue(tc)); + } + + @SuppressWarnings("unchecked") + @Test + public void indexer() throws Exception { + String[] sss = new String[]{"a","b","c"}; + Number[] ns = new Number[]{2,8,9}; + int[] is = new int[]{8,9,10}; + double[] ds = new double[]{3.0d,4.0d,5.0d}; + long[] ls = new long[]{2L,3L,4L}; + short[] ss = new short[]{(short)33,(short)44,(short)55}; + float[] fs = new float[]{6.0f,7.0f,8.0f}; + byte[] bs = new byte[]{(byte)2,(byte)3,(byte)4}; + char[] cs = new char[]{'a','b','c'}; + + // Access String (reference type) array + expression = parser.parseExpression("[0]"); + assertEquals("a",expression.getValue(sss)); + assertCanCompile(expression); + assertEquals("a",expression.getValue(sss)); + assertEquals("Ljava/lang/String",getAst().getExitDescriptor()); + + expression = parser.parseExpression("[1]"); + assertEquals(8,expression.getValue(ns)); + assertCanCompile(expression); + assertEquals(8,expression.getValue(ns)); + assertEquals("Ljava/lang/Number",getAst().getExitDescriptor()); + + // Access int array + expression = parser.parseExpression("[2]"); + assertEquals(10,expression.getValue(is)); + assertCanCompile(expression); + assertEquals(10,expression.getValue(is)); + assertEquals("I",getAst().getExitDescriptor()); + + // Access double array + expression = parser.parseExpression("[1]"); + assertEquals(4.0d,expression.getValue(ds)); + assertCanCompile(expression); + assertEquals(4.0d,expression.getValue(ds)); + assertEquals("D",getAst().getExitDescriptor()); + + // Access long array + expression = parser.parseExpression("[0]"); + assertEquals(2L,expression.getValue(ls)); + assertCanCompile(expression); + assertEquals(2L,expression.getValue(ls)); + assertEquals("J",getAst().getExitDescriptor()); + + // Access short array + expression = parser.parseExpression("[2]"); + assertEquals((short)55,expression.getValue(ss)); + assertCanCompile(expression); + assertEquals((short)55,expression.getValue(ss)); + assertEquals("S",getAst().getExitDescriptor()); + + // Access float array + expression = parser.parseExpression("[0]"); + assertEquals(6.0f,expression.getValue(fs)); + assertCanCompile(expression); + assertEquals(6.0f,expression.getValue(fs)); + assertEquals("F",getAst().getExitDescriptor()); + + // Access byte array + expression = parser.parseExpression("[2]"); + assertEquals((byte)4,expression.getValue(bs)); + assertCanCompile(expression); + assertEquals((byte)4,expression.getValue(bs)); + assertEquals("B",getAst().getExitDescriptor()); + + // Access char array + expression = parser.parseExpression("[1]"); + assertEquals('b',expression.getValue(cs)); + assertCanCompile(expression); + assertEquals('b',expression.getValue(cs)); + assertEquals("C",getAst().getExitDescriptor()); + + // Collections + List strings = new ArrayList(); + strings.add("aaa"); + strings.add("bbb"); + strings.add("ccc"); + expression = parser.parseExpression("[1]"); + assertEquals("bbb",expression.getValue(strings)); + assertCanCompile(expression); + assertEquals("bbb",expression.getValue(strings)); + assertEquals("Ljava/lang/String",getAst().getExitDescriptor()); + + List ints = new ArrayList(); + ints.add(123); + ints.add(456); + ints.add(789); + expression = parser.parseExpression("[2]"); + assertEquals(789,expression.getValue(ints)); + assertCanCompile(expression); + assertEquals(789,expression.getValue(ints)); + assertEquals("Ljava/lang/Integer",getAst().getExitDescriptor()); + + // Maps + Map map1 = new HashMap(); + map1.put("aaa", 111); + map1.put("bbb", 222); + map1.put("ccc", 333); + expression = parser.parseExpression("['aaa']"); + assertEquals(111,expression.getValue(map1)); + assertCanCompile(expression); + assertEquals(111,expression.getValue(map1)); + assertEquals("Ljava/lang/Integer",getAst().getExitDescriptor()); + + // Object + TestClass6 tc = new TestClass6(); + expression = parser.parseExpression("['orange']"); + assertEquals("value1",expression.getValue(tc)); + assertCanCompile(expression); + assertEquals("value1",expression.getValue(tc)); + assertEquals("Ljava/lang/String",getAst().getExitDescriptor()); + + expression = parser.parseExpression("['peach']"); + assertEquals(34L,expression.getValue(tc)); + assertCanCompile(expression); + assertEquals(34L,expression.getValue(tc)); + assertEquals("J",getAst().getExitDescriptor()); + + // getter + expression = parser.parseExpression("['banana']"); + assertEquals("value3",expression.getValue(tc)); + assertCanCompile(expression); + assertEquals("value3",expression.getValue(tc)); + assertEquals("Ljava/lang/String",getAst().getExitDescriptor()); + + // list of arrays + + List listOfStringArrays = new ArrayList(); + listOfStringArrays.add(new String[]{"a","b","c"}); + listOfStringArrays.add(new String[]{"d","e","f"}); + expression = parser.parseExpression("[1]"); + assertEquals("d e f",stringify(expression.getValue(listOfStringArrays))); + assertCanCompile(expression); + assertEquals("d e f",stringify(expression.getValue(listOfStringArrays))); + assertEquals("[Ljava/lang/String",getAst().getExitDescriptor()); + + List listOfIntegerArrays = new ArrayList(); + listOfIntegerArrays.add(new Integer[]{1,2,3}); + listOfIntegerArrays.add(new Integer[]{4,5,6}); + expression = parser.parseExpression("[0]"); + assertEquals("1 2 3",stringify(expression.getValue(listOfIntegerArrays))); + assertCanCompile(expression); + assertEquals("1 2 3",stringify(expression.getValue(listOfIntegerArrays))); + assertEquals("[Ljava/lang/Integer",getAst().getExitDescriptor()); + + expression = parser.parseExpression("[0][1]"); + assertEquals(2,expression.getValue(listOfIntegerArrays)); + assertCanCompile(expression); + assertEquals(2,expression.getValue(listOfIntegerArrays)); + assertEquals("Ljava/lang/Integer",getAst().getExitDescriptor()); + + // array of lists + List[] stringArrayOfLists = new ArrayList[2]; + stringArrayOfLists[0] = new ArrayList(); + stringArrayOfLists[0].add("a"); + stringArrayOfLists[0].add("b"); + stringArrayOfLists[0].add("c"); + stringArrayOfLists[1] = new ArrayList(); + stringArrayOfLists[1].add("d"); + stringArrayOfLists[1].add("e"); + stringArrayOfLists[1].add("f"); + expression = parser.parseExpression("[1]"); + assertEquals("d e f",stringify(expression.getValue(stringArrayOfLists))); + assertCanCompile(expression); + assertEquals("d e f",stringify(expression.getValue(stringArrayOfLists))); + assertEquals("Ljava/util/ArrayList",getAst().getExitDescriptor()); + + expression = parser.parseExpression("[1][2]"); + assertEquals("f",stringify(expression.getValue(stringArrayOfLists))); + assertCanCompile(expression); + assertEquals("f",stringify(expression.getValue(stringArrayOfLists))); + assertEquals("Ljava/lang/String",getAst().getExitDescriptor()); + + // array of arrays + String[][] referenceTypeArrayOfArrays = new String[][]{new String[]{"a","b","c"},new String[]{"d","e","f"}}; + expression = parser.parseExpression("[1]"); + assertEquals("d e f",stringify(expression.getValue(referenceTypeArrayOfArrays))); + assertCanCompile(expression); + assertEquals("[Ljava/lang/String",getAst().getExitDescriptor()); + assertEquals("d e f",stringify(expression.getValue(referenceTypeArrayOfArrays))); + assertEquals("[Ljava/lang/String",getAst().getExitDescriptor()); + + expression = parser.parseExpression("[1][2]"); + assertEquals("f",stringify(expression.getValue(referenceTypeArrayOfArrays))); + assertCanCompile(expression); + assertEquals("f",stringify(expression.getValue(referenceTypeArrayOfArrays))); + assertEquals("Ljava/lang/String",getAst().getExitDescriptor()); + + int[][] primitiveTypeArrayOfArrays = new int[][]{new int[]{1,2,3},new int[]{4,5,6}}; + expression = parser.parseExpression("[1]"); + assertEquals("4 5 6",stringify(expression.getValue(primitiveTypeArrayOfArrays))); + assertCanCompile(expression); + assertEquals("4 5 6",stringify(expression.getValue(primitiveTypeArrayOfArrays))); + assertEquals("[I",getAst().getExitDescriptor()); + + expression = parser.parseExpression("[1][2]"); + assertEquals("6",stringify(expression.getValue(primitiveTypeArrayOfArrays))); + assertCanCompile(expression); + assertEquals("6",stringify(expression.getValue(primitiveTypeArrayOfArrays))); + assertEquals("I",getAst().getExitDescriptor()); + + // list of lists of reference types + List> listOfListOfStrings = new ArrayList>(); + List list = new ArrayList(); + list.add("a"); + list.add("b"); + list.add("c"); + listOfListOfStrings.add(list); + list = new ArrayList(); + list.add("d"); + list.add("e"); + list.add("f"); + listOfListOfStrings.add(list); + + expression = parser.parseExpression("[1]"); + assertEquals("d e f",stringify(expression.getValue(listOfListOfStrings))); + assertCanCompile(expression); + assertEquals("Ljava/util/ArrayList",getAst().getExitDescriptor()); + assertEquals("d e f",stringify(expression.getValue(listOfListOfStrings))); + assertEquals("Ljava/util/ArrayList",getAst().getExitDescriptor()); + + expression = parser.parseExpression("[1][2]"); + assertEquals("f",stringify(expression.getValue(listOfListOfStrings))); + assertCanCompile(expression); + assertEquals("f",stringify(expression.getValue(listOfListOfStrings))); + assertEquals("Ljava/lang/String",getAst().getExitDescriptor()); + + // Map of lists + Map> mapToLists = new HashMap>(); + list = new ArrayList(); + list.add("a"); + list.add("b"); + list.add("c"); + mapToLists.put("foo", list); + expression = parser.parseExpression("['foo']"); + assertEquals("a b c",stringify(expression.getValue(mapToLists))); + assertCanCompile(expression); + assertEquals("Ljava/util/ArrayList",getAst().getExitDescriptor()); + assertEquals("a b c",stringify(expression.getValue(mapToLists))); + assertEquals("Ljava/util/ArrayList",getAst().getExitDescriptor()); + + expression = parser.parseExpression("['foo'][2]"); + assertEquals("c",stringify(expression.getValue(mapToLists))); + assertCanCompile(expression); + assertEquals("c",stringify(expression.getValue(mapToLists))); + assertEquals("Ljava/lang/String",getAst().getExitDescriptor()); + + // Map to array + Map mapToIntArray = new HashMap(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.addPropertyAccessor(new CompilableMapAccessor()); + mapToIntArray.put("foo",new int[]{1,2,3}); + expression = parser.parseExpression("['foo']"); + assertEquals("1 2 3",stringify(expression.getValue(mapToIntArray))); + assertCanCompile(expression); + assertEquals("[I",getAst().getExitDescriptor()); + assertEquals("1 2 3",stringify(expression.getValue(mapToIntArray))); + assertEquals("[I",getAst().getExitDescriptor()); + + expression = parser.parseExpression("['foo'][1]"); + assertEquals(2,expression.getValue(mapToIntArray)); + assertCanCompile(expression); + assertEquals(2,expression.getValue(mapToIntArray)); + + expression = parser.parseExpression("foo"); + assertEquals("1 2 3",stringify(expression.getValue(ctx,mapToIntArray))); + assertCanCompile(expression); + assertEquals("1 2 3",stringify(expression.getValue(ctx,mapToIntArray))); + assertEquals("Ljava/lang/Object",getAst().getExitDescriptor()); + + expression = parser.parseExpression("foo[1]"); + assertEquals(2,expression.getValue(ctx,mapToIntArray)); + assertCanCompile(expression); + assertEquals(2,expression.getValue(ctx,mapToIntArray)); + + expression = parser.parseExpression("['foo'][2]"); + assertEquals("3",stringify(expression.getValue(ctx,mapToIntArray))); + assertCanCompile(expression); + assertEquals("3",stringify(expression.getValue(ctx,mapToIntArray))); + assertEquals("I",getAst().getExitDescriptor()); + + // Map array + Map[] mapArray = new Map[1]; + mapArray[0] = new HashMap(); + mapArray[0].put("key", "value1"); + expression = parser.parseExpression("[0]"); + assertEquals("{key=value1}",stringify(expression.getValue(mapArray))); + assertCanCompile(expression); + assertEquals("Ljava/util/Map",getAst().getExitDescriptor()); + assertEquals("{key=value1}",stringify(expression.getValue(mapArray))); + assertEquals("Ljava/util/Map",getAst().getExitDescriptor()); + + expression = parser.parseExpression("[0]['key']"); + assertEquals("value1",stringify(expression.getValue(mapArray))); + assertCanCompile(expression); + assertEquals("value1",stringify(expression.getValue(mapArray))); + assertEquals("Ljava/lang/String",getAst().getExitDescriptor()); + } + + @Test + public void mixingItUp_propertyAccessIndexerOpLtTernaryRootNull() throws Exception { + Payload payload = new Payload(); + + expression = parser.parseExpression("DR[0].three"); + Object v = expression.getValue(payload); + assertEquals("Lorg/springframework/expression/spel/SpelCompilationCoverageTests$Three",getAst().getExitDescriptor()); + + Expression expression = parser.parseExpression("DR[0].three.four lt 0.1d?#root:null"); + v = expression.getValue(payload); + + SpelExpression sExpr = (SpelExpression)expression; + Ternary ternary = (Ternary)sExpr.getAST(); + OpLT oplt = (OpLT)ternary.getChild(0); + CompoundExpression cExpr = (CompoundExpression)oplt.getLeftOperand(); + String cExprExitDescriptor = cExpr.getExitDescriptor(); + assertEquals("D",cExprExitDescriptor); + assertEquals("Z",oplt.getExitDescriptor()); + + assertCanCompile(expression); + Object vc = expression.getValue(payload); + assertEquals(payload,v); + assertEquals(payload,vc); + payload.DR[0].three.four = 0.13d; + vc = expression.getValue(payload); + assertNull(vc); + } + + static class MyAccessor implements CompilablePropertyAccessor { + + private Method method; + + public Class[] getSpecificTargetClasses() { + return new Class[]{Payload2.class}; + } + + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + // target is a Payload2 instance + return true; + } + + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + Payload2 payload2 = (Payload2)target; + return new TypedValue(payload2.getField(name)); + } + + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return false; + } + + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(PropertyOrFieldReference propertyReference, MethodVisitor mv,CodeFlow codeflow) { + if (method == null) { + try { + method = Payload2.class.getDeclaredMethod("getField", String.class); + } catch (Exception e) {} + } + String descriptor = codeflow.lastDescriptor(); + String memberDeclaringClassSlashedDescriptor = method.getDeclaringClass().getName().replace('.','/'); + if (descriptor == null) { + codeflow.loadTarget(mv); + } + if (descriptor == null || !memberDeclaringClassSlashedDescriptor.equals(descriptor.substring(1))) { + mv.visitTypeInsn(CHECKCAST, memberDeclaringClassSlashedDescriptor); + } + mv.visitLdcInsn(propertyReference.getName()); + mv.visitMethodInsn(INVOKEVIRTUAL, memberDeclaringClassSlashedDescriptor, method.getName(),CodeFlow.createSignatureDescriptor(method),false); + } + + @Override + public Class getPropertyType() { + return Object.class; + } + + } + + @Test + public void variantGetter() throws Exception { + Payload2Holder holder = new Payload2Holder(); + StandardEvaluationContext ctx = new StandardEvaluationContext(); + ctx.addPropertyAccessor(new MyAccessor()); + expression = parser.parseExpression("payload2.var1"); + Object v = expression.getValue(ctx,holder); + assertEquals("abc",v); + +// // time it interpreted +// long stime = System.currentTimeMillis(); +// for (int i=0;i<100000;i++) { +// v = expression.getValue(ctx,holder); +// } +// System.out.println((System.currentTimeMillis()-stime)); +// + assertCanCompile(expression); + v = expression.getValue(ctx,holder); + assertEquals("abc",v); +// +// // time it compiled +// stime = System.currentTimeMillis(); +// for (int i=0;i<100000;i++) { +// v = expression.getValue(ctx,holder); +// } +// System.out.println((System.currentTimeMillis()-stime)); + + } + + static class CompilableMapAccessor implements CompilablePropertyAccessor { + + @Override + public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException { + Map map = (Map) target; + return map.containsKey(name); + } + + @Override + public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException { + Map map = (Map) target; + Object value = map.get(name); + if (value == null && !map.containsKey(name)) { + throw new MapAccessException(name); + } + return new TypedValue(value); + } + + @Override + public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { + return true; + } + + @Override + @SuppressWarnings("unchecked") + public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + Map map = (Map) target; + map.put(name, newValue); + } + + @Override + public Class[] getSpecificTargetClasses() { + return new Class[] {Map.class}; + } + + + /** + * Exception thrown from {@code read} in order to reset a cached + * PropertyAccessor, allowing other accessors to have a try. + */ + @SuppressWarnings("serial") + private static class MapAccessException extends AccessException { + + private final String key; + + public MapAccessException(String key) { + super(null); + this.key = key; + } + + @Override + public String getMessage() { + return "Map does not contain a value for key '" + this.key + "'"; + } + } + + @Override + public boolean isCompilable() { + return true; + } + + @Override + public void generateCode(PropertyOrFieldReference propertyReference, + MethodVisitor mv, CodeFlow codeflow) { + String descriptor = codeflow.lastDescriptor(); + if (descriptor == null) { + codeflow.loadTarget(mv); + } + mv.visitLdcInsn(propertyReference.getName()); + mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get","(Ljava/lang/Object;)Ljava/lang/Object;",true); + +// if (method == null) { +// try { +// method = Payload2.class.getDeclaredMethod("getField", String.class); +// } catch (Exception e) {} +// } +// String descriptor = codeflow.lastDescriptor(); +// String memberDeclaringClassSlashedDescriptor = method.getDeclaringClass().getName().replace('.','/'); +// if (descriptor == null) { +// codeflow.loadTarget(mv); +// } +// if (descriptor == null || !memberDeclaringClassSlashedDescriptor.equals(descriptor.substring(1))) { +// mv.visitTypeInsn(CHECKCAST, memberDeclaringClassSlashedDescriptor); +// } +// mv.visitLdcInsn(propertyReference.getName()); +// mv.visitMethodInsn(INVOKEVIRTUAL, memberDeclaringClassSlashedDescriptor, method.getName(),CodeFlow.createDescriptor(method)); +// 6: invokeinterface #6, 2; //InterfaceMethod java/util/Map.get:(Ljava/lang/Object;)Ljava/lang/Object; + } + + @Override + public Class getPropertyType() { + return Object.class; + } + + } + + + // helpers + + private SpelNodeImpl getAst() { + SpelExpression spelExpression = (SpelExpression)expression; + SpelNode ast = spelExpression.getAST(); + return (SpelNodeImpl)ast; + } + + private String stringify(Object object) { + StringBuilder s = new StringBuilder(); + if (object instanceof List) { + List ls = (List)object; + for (Object l: ls) { + s.append(l); + s.append(" "); + } + } + else if (object instanceof Object[]) { + Object[] os = (Object[])object; + for (Object o: os) { + s.append(o); + s.append(" "); + } + } + else if (object instanceof int[]) { + int[] is = (int[])object; + for (int i: is) { + s.append(i); + s.append(" "); + } + } + else { + s.append(object.toString()); + } + return s.toString().trim(); + } + + private void assertCanCompile(Expression expression) { + assertTrue(SpelCompiler.compile(expression)); + } + + private void assertCantCompile(Expression expression) { + assertFalse(SpelCompiler.compile(expression)); + } + + private Expression parse(String expression) { + return parser.parseExpression(expression); + } + + private void assertGetValueFail(Expression expression) { + try { + Object o = expression.getValue(); + fail("Calling getValue on the expression should have failed but returned "+o); + } catch (Exception ex) { + // success! + } + } + + // test classes + + public static class Payload { + Two[] DR = new Two[]{new Two()}; + public Two holder = new Two(); + + public Two[] getDR() { + return DR; + } + } + + public static class Payload2 { + String var1 = "abc"; + String var2 = "def"; + public Object getField(String name) { + if (name.equals("var1")) { + return var1; + } else if (name.equals("var2")) { + return var2; + } + return null; + } + } + + public static class Payload2Holder { + public Payload2 payload2 = new Payload2(); + } + + public static class Two { + Three three = new Three(); + public Three getThree() { + return three; + } + public String toString() { + return "instanceof Two"; + } + } + + public static class Three { + double four = 0.04d; + public double getFour() { + return four; + } + } + + public static class TestClass1 { + public int index1 = 1; + public int index2 = 3; + public String word = "abcd"; + } + + public static class TestClass4 { + public boolean a,b; + public boolean gettrue() { return true; } + public boolean getfalse() { return false; } + public boolean getA() { return a; } + public boolean getB() { return b; } + } + + public static class TestClass5 { + public int i = 0; + public String s = null; + public static int _i = 0; + public static String _s = null; + + public Object obj = null; + + public String field = null; + + public void reset() { + i = 0; + _i=0; + s = null; + _s = null; + field = null; + } + + public void one() { i = 1; } + + public static void two() { _i = 1; } + + public String three() { return "hello"; } + public long four() { return 3277700L; } + + public static String five() { return "hello"; } + public static long six() { return 3277700L; } + + public void seven(String toset) { s = toset; } +// public void seven(Number n) { s = n.toString(); } + + public void takeNumber(Number n) { s = n.toString(); } + public void takeString(String s) { this.s = s; } + public static void eight(String toset) { _s = toset; } + + public void nine(int toset) { i = toset; } + public static void ten(int toset) { _i = toset; } + + public void eleven(String... vargs) { + if (vargs==null) { + s = ""; + } + else { + s = ""; + for (String varg: vargs) { + s+=varg; + } + } + } + + public void twelve(int... vargs) { + if (vargs==null) { + i = 0; + } + else { + i = 0; + for (int varg: vargs) { + i+=varg; + } + } + } + } + + public static class TestClass6 { + public String orange = "value1"; + public static String apple = "value2"; + + public long peach = 34L; + + public String getBanana() { + return "value3"; + } + + public static String getPlum() { + return "value4"; + } + } + + public static class TestClass7 { + public static String property; + static { + String s = "UK 123"; + StringTokenizer st = new StringTokenizer(s); + property = st.nextToken(); + } + + public static void reset() { + String s = "UK 123"; + StringTokenizer st = new StringTokenizer(s); + property = st.nextToken(); + } + + } + + public static class TestClass8 { + public int i; + public String s; + public double d; + public boolean z; + + public TestClass8(int i, String s, double d, boolean z) { + this.i = i; + this.s = s; + this.d = d; + this.z = z; + } + + public TestClass8() { + + } + + public TestClass8(Integer i) { + this.i = i; + } + + @SuppressWarnings("unused") + private TestClass8(String a, String b) { + this.s = a+b; + } + } + + @SuppressWarnings("unused") + private static class TestClass9 { + public TestClass9(int i) {} + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationPerformanceTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationPerformanceTests.java new file mode 100644 index 00000000000..8d904a6b83d --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationPerformanceTests.java @@ -0,0 +1,471 @@ +/* + * Copyright 2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression.spel; + +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelCompiler; + +import static org.junit.Assert.*; + +/** + * Checks the speed of compiled SpEL expressions. + * By default these tests are marked Ignore since they can fail on a busy machine because they + * compare relative performance of interpreted vs compiled. + * + * @author Andy Clement + * @since 4.1 + */ +@Ignore +public class SpelCompilationPerformanceTests extends AbstractExpressionTests { + + int count = 50000; // Number of evaluations that are timed in one run + int iterations = 10; // Number of times to repeat 'count' evaluations (for averaging) + private final static boolean noisyTests = true; + + Expression expression; + + public static class Payload { + Two[] DR = new Two[]{new Two()}; + + public Two[] getDR() { + return DR; + } + } + + public static class Two { + Three DRFixedSection = new Three(); + public Three getDRFixedSection() { + return DRFixedSection; + } + } + + public static class Three { + double duration = 0.4d; + public double getDuration() { + return duration; + } + } + + @Test + public void complexExpressionPerformance() throws Exception { + Payload payload = new Payload(); + Expression expression = parser.parseExpression("DR[0].DRFixedSection.duration lt 0.1"); + boolean b = false; + long iTotal = 0,cTotal = 0; + + // warmup + for (int i=0;i=interpretedTotal) { + fail("Compiled version is slower than interpreted!"); + } + } + + + + + public static class TestClass2 { + public String name = "Santa"; + private String name2 = "foobar"; + public String getName2() { + return name2; + } + public Foo foo = new Foo(); + public static class Foo { + public Bar bar = new Bar(); + Bar b = new Bar(); + public Bar getBaz() { + return b; + } + public Bar bay() { + return b; + } + } + public static class Bar { + public String boo = "oranges"; + } + } + + @Test + public void compilingPropertyReferenceField() throws Exception { + long interpretedTotal = 0, compiledTotal = 0, stime, etime; + String interpretedResult = null, compiledResult = null; + + TestClass2 testdata = new TestClass2(); + Expression expression = parser.parseExpression("name"); + + // warmup + for (int i=0;i=interpretedTotal) { + fail("Compiled version is slower than interpreted!"); + } + } + + // --- + + private void reportPerformance(String title, long interpretedTotal, long compiledTotal) { + double averageInterpreted = interpretedTotal/(iterations); + double averageCompiled = compiledTotal/(iterations); + double ratio = (averageCompiled/averageInterpreted)*100.0d; + logln(">>"+title+": average for "+count+": compiled="+averageCompiled+"ms interpreted="+averageInterpreted+"ms: compiled takes "+((int)ratio)+"% of the interpreted time"); + if (averageCompiled>averageInterpreted) { + fail("Compiled version took longer than interpreted! CompiledSpeed=~"+averageCompiled+ + "ms InterpretedSpeed="+averageInterpreted+"ms"); + } + logln(); + } + + private void log(String message) { + if (noisyTests) { + System.out.print(message); + } + } + + private void logln(String... message) { + if (noisyTests) { + if (message!=null && message.length>0) { + System.out.println(message[0]); + } else { + System.out.println(); + } + } + } + + private void compile(Expression expression) { + assertTrue(SpelCompiler.compile(expression)); + } +}