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}