47 changed files with 5964 additions and 90 deletions
@ -0,0 +1,53 @@
@@ -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(); |
||||
} |
||||
@ -0,0 +1,38 @@
@@ -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; |
||||
|
||||
} |
||||
@ -0,0 +1,45 @@
@@ -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 |
||||
} |
||||
@ -0,0 +1,603 @@
@@ -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<ArrayList<String>> compilationScopes; |
||||
|
||||
public CodeFlow() { |
||||
compilationScopes = new Stack<ArrayList<String>>(); |
||||
compilationScopes.add(new ArrayList<String>()); |
||||
} |
||||
|
||||
/** |
||||
* 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<String>()); |
||||
} |
||||
|
||||
/** |
||||
* 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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,277 @@
@@ -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. |
||||
* <p> |
||||
* 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. |
||||
* <p> |
||||
* 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. |
||||
* <p> |
||||
* Individual expressions can be compiled by calling |
||||
* <tt>SpelCompiler.compile(expression)</tt>. |
||||
* |
||||
* @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<ClassLoader,SpelCompiler> compilers = Collections.synchronizedMap(new WeakHashMap<ClassLoader,SpelCompiler>()); |
||||
|
||||
// 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<? extends CompiledExpression> 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<? extends CompiledExpression> 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, "<init>", "()V", null, null); |
||||
mv.visitCode(); |
||||
mv.visitVarInsn(ALOAD, 0); |
||||
mv.visitMethodInsn(INVOKESPECIAL, "org/springframework/expression/spel/CompiledExpression", "<init>", "()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<? extends CompiledExpression> clazz = (Class<? extends CompiledExpression>) 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(); |
||||
} |
||||
} |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,471 @@
@@ -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<count;i++) { |
||||
b = expression.getValue(payload,Boolean.TYPE); |
||||
} |
||||
|
||||
log("timing interpreted: "); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
long stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
b = expression.getValue(payload,Boolean.TYPE); |
||||
} |
||||
long etime = System.currentTimeMillis(); |
||||
long interpretedSpeed = (etime - stime); |
||||
iTotal+=interpretedSpeed; |
||||
log(interpretedSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
compile(expression); |
||||
boolean bc = false; |
||||
expression.getValue(payload,Boolean.TYPE); |
||||
log("timing compiled: "); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
long stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
bc = expression.getValue(payload,Boolean.TYPE); |
||||
} |
||||
long etime = System.currentTimeMillis(); |
||||
long compiledSpeed = (etime - stime); |
||||
cTotal+=compiledSpeed; |
||||
log(compiledSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
reportPerformance("complex expression",iTotal, cTotal); |
||||
|
||||
// Verify the result
|
||||
assertFalse(b); |
||||
|
||||
// Verify the same result for compiled vs interpreted
|
||||
assertEquals(b,bc); |
||||
|
||||
// Verify if the input changes, the result changes
|
||||
payload.DR[0].DRFixedSection.duration = 0.04d; |
||||
bc = expression.getValue(payload,Boolean.TYPE); |
||||
assertTrue(bc); |
||||
} |
||||
|
||||
public static class HW { |
||||
public String hello() { |
||||
return "foobar"; |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void compilingMethodReference() throws Exception { |
||||
long interpretedTotal = 0, compiledTotal = 0; |
||||
long stime,etime; |
||||
String interpretedResult = null,compiledResult = null; |
||||
|
||||
HW testdata = new HW(); |
||||
Expression expression = parser.parseExpression("hello()"); |
||||
|
||||
// warmup
|
||||
for (int i=0;i<count;i++) { |
||||
interpretedResult = expression.getValue(testdata,String.class); |
||||
} |
||||
|
||||
log("timing interpreted: "); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
interpretedResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long interpretedSpeed = (etime - stime); |
||||
interpretedTotal+=interpretedSpeed; |
||||
log(interpretedSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
compile(expression); |
||||
|
||||
log("timing compiled: "); |
||||
expression.getValue(testdata,String.class); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
compiledResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long compiledSpeed = (etime - stime); |
||||
compiledTotal+=compiledSpeed; |
||||
log(compiledSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
assertEquals(interpretedResult,compiledResult); |
||||
reportPerformance("method reference", interpretedTotal, compiledTotal); |
||||
if (compiledTotal>=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<count;i++) { |
||||
expression.getValue(testdata,String.class); |
||||
} |
||||
|
||||
log("timing interpreted: "); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
interpretedResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long interpretedSpeed = (etime - stime); |
||||
interpretedTotal+=interpretedSpeed; |
||||
log(interpretedSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
compile(expression); |
||||
|
||||
log("timing compiled: "); |
||||
expression.getValue(testdata,String.class); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
compiledResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long compiledSpeed = (etime - stime); |
||||
compiledTotal+=compiledSpeed; |
||||
log(compiledSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
assertEquals(interpretedResult,compiledResult); |
||||
reportPerformance("property reference (field)",interpretedTotal, compiledTotal); |
||||
} |
||||
|
||||
@Test |
||||
public void compilingPropertyReferenceNestedField() throws Exception { |
||||
long interpretedTotal = 0, compiledTotal = 0, stime, etime; |
||||
String interpretedResult = null, compiledResult = null; |
||||
|
||||
TestClass2 testdata = new TestClass2(); |
||||
|
||||
Expression expression = parser.parseExpression("foo.bar.boo"); |
||||
|
||||
// warmup
|
||||
for (int i=0;i<count;i++) { |
||||
expression.getValue(testdata,String.class); |
||||
} |
||||
|
||||
log("timing interpreted: "); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
interpretedResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long interpretedSpeed = (etime - stime); |
||||
interpretedTotal+=interpretedSpeed; |
||||
log(interpretedSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
compile(expression); |
||||
|
||||
log("timing compiled: "); |
||||
expression.getValue(testdata,String.class); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
compiledResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long compiledSpeed = (etime - stime); |
||||
compiledTotal+=compiledSpeed; |
||||
log(compiledSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
assertEquals(interpretedResult,compiledResult); |
||||
reportPerformance("property reference (nested field)",interpretedTotal, compiledTotal); |
||||
} |
||||
|
||||
@Test |
||||
public void compilingPropertyReferenceNestedMixedFieldGetter() throws Exception { |
||||
long interpretedTotal = 0, compiledTotal = 0, stime, etime; |
||||
String interpretedResult = null, compiledResult = null; |
||||
|
||||
TestClass2 testdata = new TestClass2(); |
||||
Expression expression = parser.parseExpression("foo.baz.boo"); |
||||
|
||||
// warmup
|
||||
for (int i=0;i<count;i++) { |
||||
expression.getValue(testdata,String.class); |
||||
} |
||||
log("timing interpreted: "); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
interpretedResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long interpretedSpeed = (etime - stime); |
||||
interpretedTotal+=interpretedSpeed; |
||||
log(interpretedSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
compile(expression); |
||||
|
||||
log("timing compiled: "); |
||||
expression.getValue(testdata,String.class); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
compiledResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long compiledSpeed = (etime - stime); |
||||
compiledTotal+=compiledSpeed; |
||||
log(compiledSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
assertEquals(interpretedResult,compiledResult); |
||||
reportPerformance("nested property reference (mixed field/getter)",interpretedTotal, compiledTotal); |
||||
} |
||||
|
||||
@Test |
||||
public void compilingNestedMixedFieldPropertyReferenceMethodReference() throws Exception { |
||||
long interpretedTotal = 0, compiledTotal = 0, stime, etime; |
||||
String interpretedResult = null, compiledResult = null; |
||||
|
||||
TestClass2 testdata = new TestClass2(); |
||||
Expression expression = parser.parseExpression("foo.bay().boo"); |
||||
|
||||
// warmup
|
||||
for (int i=0;i<count;i++) { |
||||
expression.getValue(testdata,String.class); |
||||
} |
||||
|
||||
log("timing interpreted: "); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
interpretedResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long interpretedSpeed = (etime - stime); |
||||
interpretedTotal+=interpretedSpeed; |
||||
log(interpretedSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
compile(expression); |
||||
|
||||
log("timing compiled: "); |
||||
expression.getValue(testdata,String.class); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
compiledResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long compiledSpeed = (etime - stime); |
||||
compiledTotal+=compiledSpeed; |
||||
log(compiledSpeed+"ms "); |
||||
|
||||
} |
||||
logln(); |
||||
|
||||
assertEquals(interpretedResult,compiledResult); |
||||
reportPerformance("nested reference (mixed field/method)",interpretedTotal, compiledTotal); |
||||
} |
||||
|
||||
@Test |
||||
public void compilingPropertyReferenceGetter() throws Exception { |
||||
long interpretedTotal = 0, compiledTotal = 0, stime, etime; |
||||
String interpretedResult = null, compiledResult = null; |
||||
|
||||
TestClass2 testdata = new TestClass2(); |
||||
Expression expression = parser.parseExpression("name2"); |
||||
|
||||
// warmup
|
||||
for (int i=0;i<count;i++) { |
||||
expression.getValue(testdata,String.class); |
||||
} |
||||
|
||||
log("timing interpreted: "); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
interpretedResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long interpretedSpeed = (etime - stime); |
||||
interpretedTotal+=interpretedSpeed; |
||||
log(interpretedSpeed+"ms "); |
||||
} |
||||
logln(); |
||||
|
||||
|
||||
compile(expression); |
||||
|
||||
log("timing compiled: "); |
||||
expression.getValue(testdata,String.class); |
||||
for (int iter=0;iter<iterations;iter++) { |
||||
stime = System.currentTimeMillis(); |
||||
for (int i=0;i<count;i++) { |
||||
compiledResult = expression.getValue(testdata,String.class); |
||||
} |
||||
etime = System.currentTimeMillis(); |
||||
long compiledSpeed = (etime - stime); |
||||
compiledTotal+=compiledSpeed; |
||||
log(compiledSpeed+"ms "); |
||||
|
||||
} |
||||
logln(); |
||||
|
||||
assertEquals(interpretedResult,compiledResult); |
||||
|
||||
reportPerformance("property reference (getter)", interpretedTotal, compiledTotal); |
||||
if (compiledTotal>=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)); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue