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)); + } +}