Browse Source
With these changes an optional compiler is added for SpEL expressions. The compiler is off by default but can be enabled via the SpEL parser configuration object or system property (when SpEL is embedded and parser configuration is not possible). Not all expressions are currently handled but the common cases are and it is an extensible compilation framework. Issue: SPR-10943pull/554/head
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