001/*
002 * Copyright 2002-2020 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.ast;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.lang.reflect.Modifier;
022import java.lang.reflect.Proxy;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.List;
026
027import org.springframework.asm.Label;
028import org.springframework.asm.MethodVisitor;
029import org.springframework.core.convert.TypeDescriptor;
030import org.springframework.expression.AccessException;
031import org.springframework.expression.EvaluationContext;
032import org.springframework.expression.EvaluationException;
033import org.springframework.expression.ExpressionInvocationTargetException;
034import org.springframework.expression.MethodExecutor;
035import org.springframework.expression.MethodResolver;
036import org.springframework.expression.TypedValue;
037import org.springframework.expression.spel.CodeFlow;
038import org.springframework.expression.spel.ExpressionState;
039import org.springframework.expression.spel.SpelEvaluationException;
040import org.springframework.expression.spel.SpelMessage;
041import org.springframework.expression.spel.support.ReflectiveMethodExecutor;
042import org.springframework.expression.spel.support.ReflectiveMethodResolver;
043import org.springframework.util.ObjectUtils;
044
045/**
046 * Expression language AST node that represents a method reference.
047 *
048 * @author Andy Clement
049 * @author Juergen Hoeller
050 * @since 3.0
051 */
052public class MethodReference extends SpelNodeImpl {
053
054        private final String name;
055
056        private final boolean nullSafe;
057
058        private String originalPrimitiveExitTypeDescriptor;
059
060        private volatile CachedMethodExecutor cachedExecutor;
061
062
063        public MethodReference(boolean nullSafe, String methodName, int pos, SpelNodeImpl... arguments) {
064                super(pos, arguments);
065                this.name = methodName;
066                this.nullSafe = nullSafe;
067        }
068
069
070        public final String getName() {
071                return this.name;
072        }
073
074        @Override
075        protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
076                Object[] arguments = getArguments(state);
077                if (state.getActiveContextObject().getValue() == null) {
078                        throwIfNotNullSafe(getArgumentTypes(arguments));
079                        return ValueRef.NullValueRef.INSTANCE;
080                }
081                return new MethodValueRef(state, arguments);
082        }
083
084        @Override
085        public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
086                EvaluationContext evaluationContext = state.getEvaluationContext();
087                Object value = state.getActiveContextObject().getValue();
088                TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor();
089                Object[] arguments = getArguments(state);
090                TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments);
091                updateExitTypeDescriptor();
092                return result;
093        }
094
095        private TypedValue getValueInternal(EvaluationContext evaluationContext,
096                        Object value, TypeDescriptor targetType, Object[] arguments) {
097
098                List<TypeDescriptor> argumentTypes = getArgumentTypes(arguments);
099                if (value == null) {
100                        throwIfNotNullSafe(argumentTypes);
101                        return TypedValue.NULL;
102                }
103
104                MethodExecutor executorToUse = getCachedExecutor(evaluationContext, value, targetType, argumentTypes);
105                if (executorToUse != null) {
106                        try {
107                                return executorToUse.execute(evaluationContext, value, arguments);
108                        }
109                        catch (AccessException ex) {
110                                // Two reasons this can occur:
111                                // 1. the method invoked actually threw a real exception
112                                // 2. the method invoked was not passed the arguments it expected and
113                                //    has become 'stale'
114
115                                // In the first case we should not retry, in the second case we should see
116                                // if there is a better suited method.
117
118                                // To determine the situation, the AccessException will contain a cause.
119                                // If the cause is an InvocationTargetException, a user exception was
120                                // thrown inside the method. Otherwise the method could not be invoked.
121                                throwSimpleExceptionIfPossible(value, ex);
122
123                                // At this point we know it wasn't a user problem so worth a retry if a
124                                // better candidate can be found.
125                                this.cachedExecutor = null;
126                        }
127                }
128
129                // either there was no accessor or it no longer existed
130                executorToUse = findAccessorForMethod(argumentTypes, value, evaluationContext);
131                this.cachedExecutor = new CachedMethodExecutor(
132                                executorToUse, (value instanceof Class ? (Class<?>) value : null), targetType, argumentTypes);
133                try {
134                        return executorToUse.execute(evaluationContext, value, arguments);
135                }
136                catch (AccessException ex) {
137                        // Same unwrapping exception handling as above in above catch block
138                        throwSimpleExceptionIfPossible(value, ex);
139                        throw new SpelEvaluationException(getStartPosition(), ex,
140                                        SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, this.name,
141                                        value.getClass().getName(), ex.getMessage());
142                }
143        }
144
145        private void throwIfNotNullSafe(List<TypeDescriptor> argumentTypes) {
146                if (!this.nullSafe) {
147                        throw new SpelEvaluationException(getStartPosition(),
148                                        SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED,
149                                        FormatHelper.formatMethodForMessage(this.name, argumentTypes));
150                }
151        }
152
153        private Object[] getArguments(ExpressionState state) {
154                Object[] arguments = new Object[getChildCount()];
155                for (int i = 0; i < arguments.length; i++) {
156                        // Make the root object the active context again for evaluating the parameter expressions
157                        try {
158                                state.pushActiveContextObject(state.getScopeRootContextObject());
159                                arguments[i] = this.children[i].getValueInternal(state).getValue();
160                        }
161                        finally {
162                                state.popActiveContextObject();
163                        }
164                }
165                return arguments;
166        }
167
168        private List<TypeDescriptor> getArgumentTypes(Object... arguments) {
169                List<TypeDescriptor> descriptors = new ArrayList<TypeDescriptor>(arguments.length);
170                for (Object argument : arguments) {
171                        descriptors.add(TypeDescriptor.forObject(argument));
172                }
173                return Collections.unmodifiableList(descriptors);
174        }
175
176        private MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object value,
177                        TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
178
179                List<MethodResolver> methodResolvers = evaluationContext.getMethodResolvers();
180                if (methodResolvers == null || methodResolvers.size() != 1 ||
181                                !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) {
182                        // Not a default ReflectiveMethodResolver - don't know whether caching is valid
183                        return null;
184                }
185
186                CachedMethodExecutor executorToCheck = this.cachedExecutor;
187                if (executorToCheck != null && executorToCheck.isSuitable(value, target, argumentTypes)) {
188                        return executorToCheck.get();
189                }
190                this.cachedExecutor = null;
191                return null;
192        }
193
194        private MethodExecutor findAccessorForMethod(List<TypeDescriptor> argumentTypes, Object targetObject,
195                        EvaluationContext evaluationContext) throws SpelEvaluationException {
196
197                AccessException accessException = null;
198                List<MethodResolver> methodResolvers = evaluationContext.getMethodResolvers();
199                for (MethodResolver methodResolver : methodResolvers) {
200                        try {
201                                MethodExecutor methodExecutor = methodResolver.resolve(
202                                                evaluationContext, targetObject, this.name, argumentTypes);
203                                if (methodExecutor != null) {
204                                        return methodExecutor;
205                                }
206                        }
207                        catch (AccessException ex) {
208                                accessException = ex;
209                                break;
210                        }
211                }
212
213                String method = FormatHelper.formatMethodForMessage(this.name, argumentTypes);
214                String className = FormatHelper.formatClassNameForMessage(
215                                targetObject instanceof Class ? ((Class<?>) targetObject) : targetObject.getClass());
216                if (accessException != null) {
217                        throw new SpelEvaluationException(
218                                        getStartPosition(), accessException, SpelMessage.PROBLEM_LOCATING_METHOD, method, className);
219                }
220                else {
221                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.METHOD_NOT_FOUND, method, className);
222                }
223        }
224
225        /**
226         * Decode the AccessException, throwing a lightweight evaluation exception or,
227         * if the cause was a RuntimeException, throw the RuntimeException directly.
228         */
229        private void throwSimpleExceptionIfPossible(Object value, AccessException ex) {
230                if (ex.getCause() instanceof InvocationTargetException) {
231                        Throwable rootCause = ex.getCause().getCause();
232                        if (rootCause instanceof RuntimeException) {
233                                throw (RuntimeException) rootCause;
234                        }
235                        throw new ExpressionInvocationTargetException(getStartPosition(),
236                                        "A problem occurred when trying to execute method '" + this.name +
237                                        "' on object of type [" + value.getClass().getName() + "]", rootCause);
238                }
239        }
240
241        private void updateExitTypeDescriptor() {
242                CachedMethodExecutor executorToCheck = this.cachedExecutor;
243                if (executorToCheck != null && executorToCheck.get() instanceof ReflectiveMethodExecutor) {
244                        Method method = ((ReflectiveMethodExecutor) executorToCheck.get()).getMethod();
245                        String descriptor = CodeFlow.toDescriptor(method.getReturnType());
246                        if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) {
247                                this.originalPrimitiveExitTypeDescriptor = descriptor;
248                                this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor);
249                        }
250                        else {
251                                this.exitTypeDescriptor = descriptor;
252                        }
253                }
254        }
255
256        @Override
257        public String toStringAST() {
258                StringBuilder sb = new StringBuilder(this.name);
259                sb.append("(");
260                for (int i = 0; i < getChildCount(); i++) {
261                        if (i > 0) {
262                                sb.append(",");
263                        }
264                        sb.append(getChild(i).toStringAST());
265                }
266                sb.append(")");
267                return sb.toString();
268        }
269
270        /**
271         * A method reference is compilable if it has been resolved to a reflectively accessible method
272         * and the child nodes (arguments to the method) are also compilable.
273         */
274        @Override
275        public boolean isCompilable() {
276                CachedMethodExecutor executorToCheck = this.cachedExecutor;
277                if (executorToCheck == null || executorToCheck.hasProxyTarget() ||
278                                !(executorToCheck.get() instanceof ReflectiveMethodExecutor)) {
279                        return false;
280                }
281
282                for (SpelNodeImpl child : this.children) {
283                        if (!child.isCompilable()) {
284                                return false;
285                        }
286                }
287
288                ReflectiveMethodExecutor executor = (ReflectiveMethodExecutor) executorToCheck.get();
289                if (executor.didArgumentConversionOccur()) {
290                        return false;
291                }
292                Class<?> clazz = executor.getMethod().getDeclaringClass();
293                if (!Modifier.isPublic(clazz.getModifiers()) && executor.getPublicDeclaringClass() == null) {
294                        return false;
295                }
296
297                return true;
298        }
299
300        @Override
301        public void generateCode(MethodVisitor mv, CodeFlow cf) {
302                CachedMethodExecutor executorToCheck = this.cachedExecutor;
303                if (executorToCheck == null || !(executorToCheck.get() instanceof ReflectiveMethodExecutor)) {
304                        throw new IllegalStateException("No applicable cached executor found: " + executorToCheck);
305                }
306
307                ReflectiveMethodExecutor methodExecutor = (ReflectiveMethodExecutor) executorToCheck.get();
308                Method method = methodExecutor.getMethod();
309                boolean isStaticMethod = Modifier.isStatic(method.getModifiers());
310                String descriptor = cf.lastDescriptor();
311
312                Label skipIfNull = null;
313                if (descriptor == null && !isStaticMethod) {
314                        // Nothing on the stack but something is needed
315                        cf.loadTarget(mv);
316                }
317                if ((descriptor != null || !isStaticMethod) && this.nullSafe) {
318                        mv.visitInsn(DUP);
319                        skipIfNull = new Label();
320                        Label continueLabel = new Label();
321                        mv.visitJumpInsn(IFNONNULL, continueLabel);
322                        CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor);
323                        mv.visitJumpInsn(GOTO, skipIfNull);
324                        mv.visitLabel(continueLabel);
325                }
326                if (descriptor != null && isStaticMethod) {
327                        // Something on the stack when nothing is needed
328                        mv.visitInsn(POP);
329                }
330
331                if (CodeFlow.isPrimitive(descriptor)) {
332                        CodeFlow.insertBoxIfNecessary(mv, descriptor.charAt(0));
333                }
334
335                String classDesc = (Modifier.isPublic(method.getDeclaringClass().getModifiers()) ?
336                                method.getDeclaringClass().getName().replace('.', '/') :
337                                methodExecutor.getPublicDeclaringClass().getName().replace('.', '/'));
338                if (!isStaticMethod && (descriptor == null || !descriptor.substring(1).equals(classDesc))) {
339                        CodeFlow.insertCheckCast(mv, "L" + classDesc);
340                }
341
342                generateCodeForArguments(mv, cf, method, this.children);
343                mv.visitMethodInsn(
344                                (isStaticMethod ? INVOKESTATIC : (isJava8DefaultMethod(method) ? INVOKEINTERFACE : INVOKEVIRTUAL)),
345                                classDesc, method.getName(), CodeFlow.createSignatureDescriptor(method),
346                                method.getDeclaringClass().isInterface());
347                cf.pushDescriptor(this.exitTypeDescriptor);
348
349                if (this.originalPrimitiveExitTypeDescriptor != null) {
350                        // The output of the accessor will be a primitive but from the block above it might be null,
351                        // so to have a 'common stack' element at skipIfNull target we need to box the primitive
352                        CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor);
353                }
354                if (skipIfNull != null) {
355                        mv.visitLabel(skipIfNull);
356                }
357        }
358
359        private static boolean isJava8DefaultMethod(Method method) {
360                return (method.getDeclaringClass().isInterface() && Modifier.isPublic(method.getModifiers()) &&
361                                !Modifier.isAbstract(method.getModifiers()) && !Modifier.isStatic(method.getModifiers()));
362        }
363
364
365        private class MethodValueRef implements ValueRef {
366
367                private final EvaluationContext evaluationContext;
368
369                private final Object value;
370
371                private final TypeDescriptor targetType;
372
373                private final Object[] arguments;
374
375                public MethodValueRef(ExpressionState state, Object[] arguments) {
376                        this.evaluationContext = state.getEvaluationContext();
377                        this.value = state.getActiveContextObject().getValue();
378                        this.targetType = state.getActiveContextObject().getTypeDescriptor();
379                        this.arguments = arguments;
380                }
381
382                @Override
383                public TypedValue getValue() {
384                        TypedValue result = MethodReference.this.getValueInternal(
385                                        this.evaluationContext, this.value, this.targetType, this.arguments);
386                        updateExitTypeDescriptor();
387                        return result;
388                }
389
390                @Override
391                public void setValue(Object newValue) {
392                        throw new IllegalAccessError();
393                }
394
395                @Override
396                public boolean isWritable() {
397                        return false;
398                }
399        }
400
401
402        private static class CachedMethodExecutor {
403
404                private final MethodExecutor methodExecutor;
405
406                private final Class<?> staticClass;
407
408                private final TypeDescriptor target;
409
410                private final List<TypeDescriptor> argumentTypes;
411
412                public CachedMethodExecutor(MethodExecutor methodExecutor, Class<?> staticClass,
413                                TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
414
415                        this.methodExecutor = methodExecutor;
416                        this.staticClass = staticClass;
417                        this.target = target;
418                        this.argumentTypes = argumentTypes;
419                }
420
421                public boolean isSuitable(Object value, TypeDescriptor target, List<TypeDescriptor> argumentTypes) {
422                        return ((this.staticClass == null || this.staticClass == value) &&
423                                        ObjectUtils.nullSafeEquals(this.target, target) && this.argumentTypes.equals(argumentTypes));
424                }
425
426                public boolean hasProxyTarget() {
427                        return (this.target != null && Proxy.isProxyClass(this.target.getType()));
428                }
429
430                public MethodExecutor get() {
431                        return this.methodExecutor;
432                }
433        }
434
435}