001/* 002 * Copyright 2002-2017 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.expression.spel.standard; 018 019import java.io.File; 020import java.io.FileOutputStream; 021import java.io.IOException; 022import java.net.URL; 023import java.net.URLClassLoader; 024import java.util.Map; 025import java.util.concurrent.atomic.AtomicInteger; 026 027import org.apache.commons.logging.Log; 028import org.apache.commons.logging.LogFactory; 029 030import org.springframework.asm.ClassWriter; 031import org.springframework.asm.MethodVisitor; 032import org.springframework.asm.Opcodes; 033import org.springframework.expression.Expression; 034import org.springframework.expression.spel.CodeFlow; 035import org.springframework.expression.spel.CompiledExpression; 036import org.springframework.expression.spel.SpelParserConfiguration; 037import org.springframework.expression.spel.ast.SpelNodeImpl; 038import org.springframework.util.ClassUtils; 039import org.springframework.util.ConcurrentReferenceHashMap; 040 041/** 042 * A SpelCompiler will take a regular parsed expression and create (and load) a class 043 * containing byte code that does the same thing as that expression. The compiled form of 044 * an expression will evaluate far faster than the interpreted form. 045 * 046 * <p>The SpelCompiler is not currently handling all expression types but covers many of 047 * the common cases. The framework is extensible to cover more cases in the future. For 048 * absolute maximum speed there is *no checking* in the compiled code. The compiled 049 * version of the expression uses information learned during interpreted runs of the 050 * expression when it generates the byte code. For example if it knows that a particular 051 * property dereference always seems to return a Map then it will generate byte code that 052 * expects the result of the property dereference to be a Map. This ensures maximal 053 * performance but should the dereference result in something other than a map, the 054 * compiled expression will fail - like a ClassCastException would occur if passing data 055 * of an unexpected type in a regular Java program. 056 * 057 * <p>Due to the lack of checking there are likely some expressions that should never be 058 * compiled, for example if an expression is continuously dealing with different types of 059 * data. Due to these cases the compiler is something that must be selectively turned on 060 * for an associated SpelExpressionParser (through the {@link SpelParserConfiguration} 061 * object), it is not on by default. 062 * 063 * <p>Individual expressions can be compiled by calling {@code SpelCompiler.compile(expression)}. 064 * 065 * @author Andy Clement 066 * @since 4.1 067 */ 068public class SpelCompiler implements Opcodes { 069 070 private static final Log logger = LogFactory.getLog(SpelCompiler.class); 071 072 // A compiler is created for each classloader, it manages a child class loader of that 073 // classloader and the child is used to load the compiled expressions. 074 private static final Map<ClassLoader, SpelCompiler> compilers = 075 new ConcurrentReferenceHashMap<ClassLoader, SpelCompiler>(); 076 077 078 // The child ClassLoader used to load the compiled expression classes 079 private final ChildClassLoader ccl; 080 081 // Counter suffix for generated classes within this SpelCompiler instance 082 private final AtomicInteger suffixId = new AtomicInteger(1); 083 084 085 private SpelCompiler(ClassLoader classloader) { 086 this.ccl = new ChildClassLoader(classloader); 087 } 088 089 090 /** 091 * Attempt compilation of the supplied expression. A check is made to see 092 * if it is compilable before compilation proceeds. The check involves 093 * visiting all the nodes in the expression Ast and ensuring enough state 094 * is known about them that bytecode can be generated for them. 095 * @param expression the expression to compile 096 * @return an instance of the class implementing the compiled expression, 097 * or {@code null} if compilation is not possible 098 */ 099 public CompiledExpression compile(SpelNodeImpl expression) { 100 if (expression.isCompilable()) { 101 if (logger.isDebugEnabled()) { 102 logger.debug("SpEL: compiling " + expression.toStringAST()); 103 } 104 Class<? extends CompiledExpression> clazz = createExpressionClass(expression); 105 if (clazz != null) { 106 try { 107 return clazz.newInstance(); 108 } 109 catch (Throwable ex) { 110 throw new IllegalStateException("Failed to instantiate CompiledExpression", ex); 111 } 112 } 113 } 114 115 if (logger.isDebugEnabled()) { 116 logger.debug("SpEL: unable to compile " + expression.toStringAST()); 117 } 118 return null; 119 } 120 121 private int getNextSuffix() { 122 return this.suffixId.incrementAndGet(); 123 } 124 125 /** 126 * Generate the class that encapsulates the compiled expression and define it. 127 * The generated class will be a subtype of CompiledExpression. 128 * @param expressionToCompile the expression to be compiled 129 * @return the expression call, or {@code null} if the decision was to opt out of 130 * compilation during code generation 131 */ 132 @SuppressWarnings("unchecked") 133 private Class<? extends CompiledExpression> createExpressionClass(SpelNodeImpl expressionToCompile) { 134 // Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression' 135 String clazzName = "spel/Ex" + getNextSuffix(); 136 ClassWriter cw = new ExpressionClassWriter(); 137 cw.visit(V1_5, ACC_PUBLIC, clazzName, null, "org/springframework/expression/spel/CompiledExpression", null); 138 139 // Create default constructor 140 MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null); 141 mv.visitCode(); 142 mv.visitVarInsn(ALOAD, 0); 143 mv.visitMethodInsn(INVOKESPECIAL, "org/springframework/expression/spel/CompiledExpression", 144 "<init>", "()V", false); 145 mv.visitInsn(RETURN); 146 mv.visitMaxs(1, 1); 147 mv.visitEnd(); 148 149 // Create getValue() method 150 mv = cw.visitMethod(ACC_PUBLIC, "getValue", 151 "(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null, 152 new String[ ]{"org/springframework/expression/EvaluationException"}); 153 mv.visitCode(); 154 155 CodeFlow cf = new CodeFlow(clazzName, cw); 156 157 // Ask the expression AST to generate the body of the method 158 try { 159 expressionToCompile.generateCode(mv, cf); 160 } 161 catch (IllegalStateException ex) { 162 if (logger.isDebugEnabled()) { 163 logger.debug(expressionToCompile.getClass().getSimpleName() + 164 ".generateCode opted out of compilation: " + ex.getMessage()); 165 } 166 return null; 167 } 168 169 CodeFlow.insertBoxIfNecessary(mv, cf.lastDescriptor()); 170 if ("V".equals(cf.lastDescriptor())) { 171 mv.visitInsn(ACONST_NULL); 172 } 173 mv.visitInsn(ARETURN); 174 175 mv.visitMaxs(0, 0); // not supplied due to COMPUTE_MAXS 176 mv.visitEnd(); 177 cw.visitEnd(); 178 179 cf.finish(); 180 181 byte[] data = cw.toByteArray(); 182 // TODO need to make this conditionally occur based on a debug flag 183 // dump(expressionToCompile.toStringAST(), clazzName, data); 184 return (Class<? extends CompiledExpression>) this.ccl.defineClass(clazzName.replaceAll("/", "."), data); 185 } 186 187 188 /** 189 * Factory method for compiler instances. The returned SpelCompiler will 190 * attach a class loader as the child of the given class loader and this 191 * child will be used to load compiled expressions. 192 * @param classLoader the ClassLoader to use as the basis for compilation 193 * @return a corresponding SpelCompiler instance 194 */ 195 public static SpelCompiler getCompiler(ClassLoader classLoader) { 196 ClassLoader clToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); 197 synchronized (compilers) { 198 SpelCompiler compiler = compilers.get(clToUse); 199 if (compiler == null) { 200 compiler = new SpelCompiler(clToUse); 201 compilers.put(clToUse, compiler); 202 } 203 return compiler; 204 } 205 } 206 207 /** 208 * Request that an attempt is made to compile the specified expression. It may fail if 209 * components of the expression are not suitable for compilation or the data types 210 * involved are not suitable for compilation. Used for testing. 211 * @return true if the expression was successfully compiled 212 */ 213 public static boolean compile(Expression expression) { 214 return (expression instanceof SpelExpression && ((SpelExpression) expression).compileExpression()); 215 } 216 217 /** 218 * Request to revert to the interpreter for expression evaluation. 219 * Any compiled form is discarded but can be recreated by later recompiling again. 220 * @param expression the expression 221 */ 222 public static void revertToInterpreted(Expression expression) { 223 if (expression instanceof SpelExpression) { 224 ((SpelExpression) expression).revertToInterpreted(); 225 } 226 } 227 228 /** 229 * For debugging purposes, dump the specified byte code into a file on the disk. 230 * Not yet hooked in, needs conditionally calling based on a sys prop. 231 * @param expressionText the text of the expression compiled 232 * @param name the name of the class being used for the compiled expression 233 * @param bytecode the bytecode for the generated class 234 */ 235 @SuppressWarnings("unused") 236 private static void dump(String expressionText, String name, byte[] bytecode) { 237 String nameToUse = name.replace('.', '/'); 238 String dir = (nameToUse.indexOf('/') != -1 ? nameToUse.substring(0, nameToUse.lastIndexOf('/')) : ""); 239 String dumpLocation = null; 240 try { 241 File tempFile = File.createTempFile("tmp", null); 242 dumpLocation = tempFile + File.separator + nameToUse + ".class"; 243 tempFile.delete(); 244 File f = new File(tempFile, dir); 245 f.mkdirs(); 246 // System.out.println("Expression '" + expressionText + "' compiled code dumped to " + dumpLocation); 247 if (logger.isDebugEnabled()) { 248 logger.debug("Expression '" + expressionText + "' compiled code dumped to " + dumpLocation); 249 } 250 f = new File(dumpLocation); 251 FileOutputStream fos = new FileOutputStream(f); 252 fos.write(bytecode); 253 fos.flush(); 254 fos.close(); 255 } 256 catch (IOException ex) { 257 throw new IllegalStateException( 258 "Unexpected problem dumping class '" + nameToUse + "' into " + dumpLocation, ex); 259 } 260 } 261 262 263 /** 264 * A ChildClassLoader will load the generated compiled expression classes. 265 */ 266 private static class ChildClassLoader extends URLClassLoader { 267 268 private static final URL[] NO_URLS = new URL[0]; 269 270 public ChildClassLoader(ClassLoader classLoader) { 271 super(NO_URLS, classLoader); 272 } 273 274 public Class<?> defineClass(String name, byte[] bytes) { 275 return super.defineClass(name, bytes, 0, bytes.length); 276 } 277 } 278 279 280 private class ExpressionClassWriter extends ClassWriter { 281 282 public ExpressionClassWriter() { 283 super(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); 284 } 285 286 @Override 287 protected ClassLoader getClassLoader() { 288 return ccl; 289 } 290 } 291 292}