View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.expression.spel.standard;
18  
19  import java.io.File;
20  import java.io.FileOutputStream;
21  import java.io.IOException;
22  import java.net.URL;
23  import java.net.URLClassLoader;
24  import java.util.Map;
25  import java.util.concurrent.atomic.AtomicInteger;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  
30  import org.springframework.asm.ClassWriter;
31  import org.springframework.asm.MethodVisitor;
32  import org.springframework.asm.Opcodes;
33  import org.springframework.expression.Expression;
34  import org.springframework.expression.spel.CodeFlow;
35  import org.springframework.expression.spel.CompiledExpression;
36  import org.springframework.expression.spel.SpelParserConfiguration;
37  import org.springframework.expression.spel.ast.SpelNodeImpl;
38  import org.springframework.util.ClassUtils;
39  import org.springframework.util.ConcurrentReferenceHashMap;
40  
41  /**
42   * A SpelCompiler will take a regular parsed expression and create (and load) a class
43   * containing byte code that does the same thing as that expression. The compiled form of
44   * an expression will evaluate far faster than the interpreted form.
45   *
46   * <p>The SpelCompiler is not currently handling all expression types but covers many of
47   * the common cases. The framework is extensible to cover more cases in the future. For
48   * absolute maximum speed there is *no checking* in the compiled code. The compiled
49   * version of the expression uses information learned during interpreted runs of the
50   * expression when it generates the byte code. For example if it knows that a particular
51   * property dereference always seems to return a Map then it will generate byte code that
52   * expects the result of the property dereference to be a Map. This ensures maximal
53   * performance but should the dereference result in something other than a map, the
54   * compiled expression will fail - like a ClassCastException would occur if passing data
55   * of an unexpected type in a regular Java program.
56   *
57   * <p>Due to the lack of checking there are likely some expressions that should never be
58   * compiled, for example if an expression is continuously dealing with different types of
59   * data. Due to these cases the compiler is something that must be selectively turned on
60   * for an associated SpelExpressionParser (through the {@link SpelParserConfiguration}
61   * object), it is not on by default.
62   *
63   * <p>Individual expressions can be compiled by calling {@code SpelCompiler.compile(expression)}.
64   *
65   * @author Andy Clement
66   * @since 4.1
67   */
68  public class SpelCompiler implements Opcodes {
69  
70  	private static final Log logger = LogFactory.getLog(SpelCompiler.class);
71  
72  	// A compiler is created for each classloader, it manages a child class loader of that
73  	// classloader and the child is used to load the compiled expressions.
74  	private static final Map<ClassLoader, SpelCompiler> compilers =
75  			new ConcurrentReferenceHashMap<ClassLoader, SpelCompiler>();
76  
77  
78  	// The child ClassLoader used to load the compiled expression classes
79  	private final ChildClassLoader ccl;
80  
81  	// Counter suffix for generated classes within this SpelCompiler instance
82  	private final AtomicInteger suffixId = new AtomicInteger(1);
83  
84  
85  	private SpelCompiler(ClassLoader classloader) {
86  		this.ccl = new ChildClassLoader(classloader);
87  	}
88  
89  
90  	/**
91  	 * Attempt compilation of the supplied expression. A check is
92  	 * made to see if it is compilable before compilation proceeds. The
93  	 * check involves visiting all the nodes in the expression Ast and
94  	 * ensuring enough state is known about them that bytecode can
95  	 * be generated for them.
96  	 * @param expression the expression to compile
97  	 * @return an instance of the class implementing the compiled expression, or null
98  	 * if compilation is not possible
99  	 */
100 	public CompiledExpression compile(SpelNodeImpl expression) {
101 		if (expression.isCompilable()) {
102 			if (logger.isDebugEnabled()) {
103 				logger.debug("SpEL: compiling " + expression.toStringAST());
104 			}
105 			Class<? extends CompiledExpression> clazz = createExpressionClass(expression);
106 			if (clazz != null) {
107 				try {
108 					return clazz.newInstance();
109 				}
110 				catch (Throwable ex) {
111 					throw new IllegalStateException("Failed to instantiate CompiledExpression", ex);
112 				}
113 			}
114 		}
115 
116 		if (logger.isDebugEnabled()) {
117 			logger.debug("SpEL: unable to compile " + expression.toStringAST());
118 		}
119 		return null;
120 	}
121 
122 	private int getNextSuffix() {
123 		return this.suffixId.incrementAndGet();
124 	}
125 
126 	/**
127 	 * Generate the class that encapsulates the compiled expression and define it.
128 	 * The  generated class will be a subtype of CompiledExpression.
129 	 * @param expressionToCompile the expression to be compiled
130 	 * @return the expression call, or {@code null} if the decision was to opt out of
131 	 * compilation during code generation
132 	 */
133 	@SuppressWarnings("unchecked")
134 	private Class<? extends CompiledExpression> createExpressionClass(SpelNodeImpl expressionToCompile) {
135 		// Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression'
136 		String clazzName = "spel/Ex" + getNextSuffix();
137 		ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS|ClassWriter.COMPUTE_FRAMES);
138 		cw.visit(V1_5, ACC_PUBLIC, clazzName, null, "org/springframework/expression/spel/CompiledExpression", null);
139 
140 		// Create default constructor
141 		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
142 		mv.visitCode();
143 		mv.visitVarInsn(ALOAD, 0);
144 		mv.visitMethodInsn(INVOKESPECIAL, "org/springframework/expression/spel/CompiledExpression",
145 				"<init>", "()V", false);
146 		mv.visitInsn(RETURN);
147 		mv.visitMaxs(1, 1);
148 		mv.visitEnd();
149 
150 		// Create getValue() method
151 		mv = cw.visitMethod(ACC_PUBLIC, "getValue",
152 				"(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null,
153 				new String[ ]{"org/springframework/expression/EvaluationException"});
154 		mv.visitCode();
155 
156 		CodeFlow cf = new CodeFlow(clazzName, cw);
157 
158 		// Ask the expression AST to generate the body of the method
159 		try {
160 			expressionToCompile.generateCode(mv, cf);
161 		}
162 		catch (IllegalStateException ex) {
163 			if (logger.isDebugEnabled()) {
164 				logger.debug(expressionToCompile.getClass().getSimpleName() +
165 						".generateCode opted out of compilation: " + ex.getMessage());
166 			}
167 			return null;
168 		}
169 
170 		CodeFlow.insertBoxIfNecessary(mv, cf.lastDescriptor());
171 		if ("V".equals(cf.lastDescriptor())) {
172 			mv.visitInsn(ACONST_NULL);
173 		}
174 		mv.visitInsn(ARETURN);
175 
176 		mv.visitMaxs(0, 0);  // not supplied due to COMPUTE_MAXS
177 		mv.visitEnd();
178 		cw.visitEnd();
179 		
180 		cf.finish();
181 		
182 		byte[] data = cw.toByteArray();
183 		// TODO need to make this conditionally occur based on a debug flag
184 		// dump(expressionToCompile.toStringAST(), clazzName, data);
185 		return (Class<? extends CompiledExpression>) this.ccl.defineClass(clazzName.replaceAll("/", "."), data);
186 	}
187 
188 
189 	/**
190 	 * Factory method for compiler instances. The returned SpelCompiler will
191 	 * attach a class loader as the child of the given class loader and this
192 	 * child will be used to load compiled expressions.
193 	 * @param classLoader the ClassLoader to use as the basis for compilation
194 	 * @return a corresponding SpelCompiler instance
195 	 */
196 	public static SpelCompiler getCompiler(ClassLoader classLoader) {
197 		ClassLoader clToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
198 		synchronized (compilers) {
199 			SpelCompiler compiler = compilers.get(clToUse);
200 			if (compiler == null) {
201 				compiler = new SpelCompiler(clToUse);
202 				compilers.put(clToUse, compiler);
203 			}
204 			return compiler;
205 		}
206 	}
207 
208 	/**
209 	 * Request that an attempt is made to compile the specified expression. It may fail if
210 	 * components of the expression are not suitable for compilation or the data types
211 	 * involved are not suitable for compilation. Used for testing.
212 	 * @return true if the expression was successfully compiled
213 	 */
214 	public static boolean compile(Expression expression) {
215 		return (expression instanceof SpelExpression && ((SpelExpression) expression).compileExpression());
216 	}
217 
218 	/**
219 	 * Request to revert to the interpreter for expression evaluation.
220 	 * Any compiled form is discarded but can be recreated by later recompiling again.
221 	 * @param expression the expression
222 	 */
223 	public static void revertToInterpreted(Expression expression) {
224 		if (expression instanceof SpelExpression) {
225 			((SpelExpression) expression).revertToInterpreted();
226 		}
227 	}
228 
229 	/**
230 	 * For debugging purposes, dump the specified byte code into a file on the disk.
231 	 * Not yet hooked in, needs conditionally calling based on a sys prop.
232 	 * @param expressionText the text of the expression compiled
233 	 * @param name the name of the class being used for the compiled expression
234 	 * @param bytecode the bytecode for the generated class
235 	 */
236 	@SuppressWarnings("unused")
237 	private static void dump(String expressionText, String name, byte[] bytecode) {
238 		String nameToUse = name.replace('.', '/');
239 		String dir = (nameToUse.indexOf('/') != -1 ? nameToUse.substring(0, nameToUse.lastIndexOf('/')) : "");
240 		String dumpLocation = null;
241 		try {
242 			File tempFile = File.createTempFile("tmp", null);
243 			dumpLocation = tempFile + File.separator + nameToUse + ".class";
244 			tempFile.delete();
245 			File f = new File(tempFile, dir);
246 			f.mkdirs();
247 			// System.out.println("Expression '" + expressionText + "' compiled code dumped to " + dumpLocation);
248 			if (logger.isDebugEnabled()) {
249 				logger.debug("Expression '" + expressionText + "' compiled code dumped to " + dumpLocation);
250 			}
251 			f = new File(dumpLocation);
252 			FileOutputStream fos = new FileOutputStream(f);
253 			fos.write(bytecode);
254 			fos.flush();
255 			fos.close();
256 		}
257 		catch (IOException ex) {
258 			throw new IllegalStateException(
259 					"Unexpected problem dumping class " + nameToUse + " into " + dumpLocation, ex);
260 		}
261 	}
262 
263 
264 	/**
265 	 * A ChildClassLoader will load the generated compiled expression classes
266 	 */
267 	public static class ChildClassLoader extends URLClassLoader {
268 
269 		private static final URL[] NO_URLS = new URL[0];
270 
271 		public ChildClassLoader(ClassLoader classloader) {
272 			super(NO_URLS, classloader);
273 		}
274 
275 		public Class<?> defineClass(String name, byte[] bytes) {
276 			return super.defineClass(name, bytes, 0, bytes.length);
277 		}
278 	}
279 
280 }