diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java index d489c522c3b..8e40934cab9 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java @@ -885,7 +885,7 @@ public abstract class ClassUtils { public static Class getUserClass(Class clazz) { if (clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) { Class superclass = clazz.getSuperclass(); - if (superclass != null && Object.class != superclass) { + if (superclass != null && superclass != Object.class) { return superclass; } } @@ -1244,10 +1244,11 @@ public abstract class ClassUtils { * access (e.g. calls to {@code Class#getDeclaredMethods} etc, this implementation * will fall back to returning the originally provided method. * @param method the method to be invoked, which may come from an interface - * @param targetClass the target class for the current invocation. - * May be {@code null} or may not even implement the method. + * @param targetClass the target class for the current invocation + * (may be {@code null} or may not even implement the method) * @return the specific target method, or the original method if the - * {@code targetClass} doesn't implement it or is {@code null} + * {@code targetClass} does not implement it + * @see #getInterfaceMethodIfPossible */ public static Method getMostSpecificMethod(Method method, @Nullable Class targetClass) { if (targetClass != null && targetClass != method.getDeclaringClass() && isOverridable(method, targetClass)) { @@ -1273,6 +1274,34 @@ public abstract class ClassUtils { return method; } + /** + * Determine a corresponding interface method for the given method handle, if possible. + *

This is particularly useful for arriving at a public exported type on Jigsaw + * which can be reflectively invoked without an illegal access warning. + * @param method the method to be invoked, potentially from an implementation class + * @return the corresponding interface method, or the original method if none found + * @since 5.1 + * @see #getMostSpecificMethod + */ + public static Method getInterfaceMethodIfPossible(Method method) { + if (Modifier.isPublic(method.getModifiers()) && !method.getDeclaringClass().isInterface()) { + Class current = method.getDeclaringClass(); + while (current != null && current != Object.class) { + Class[] ifcs = current.getInterfaces(); + for (Class ifc : ifcs) { + try { + return ifc.getMethod(method.getName(), method.getParameterTypes()); + } + catch (NoSuchMethodException ex) { + // ignore + } + } + current = current.getSuperclass(); + } + } + return method; + } + /** * Determine whether the given method is declared by the user or at least pointing to * a user-declared method. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index ee265c072e6..9dd732e535e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -182,8 +182,7 @@ public class MethodReference extends SpelNodeImpl { @Nullable TypeDescriptor target, List argumentTypes) { List methodResolvers = evaluationContext.getMethodResolvers(); - if (methodResolvers.size() != 1 || - !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) { + if (methodResolvers.size() != 1 || !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) { // Not a default ReflectiveMethodResolver - don't know whether caching is valid return null; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java index bc3ccd060cd..b91ee4b8032 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java @@ -26,6 +26,7 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.MethodExecutor; import org.springframework.expression.TypedValue; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; /** @@ -37,7 +38,9 @@ import org.springframework.util.ReflectionUtils; */ public class ReflectiveMethodExecutor implements MethodExecutor { - private final Method method; + private final Method originalMethod; + + private final Method methodToInvoke; @Nullable private final Integer varargsPosition; @@ -50,8 +53,13 @@ public class ReflectiveMethodExecutor implements MethodExecutor { private boolean argumentConversionOccurred = false; + /** + * Create a new executor for the given method. + * @param method the method to invoke + */ public ReflectiveMethodExecutor(Method method) { - this.method = method; + this.originalMethod = method; + this.methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(method); if (method.isVarArgs()) { Class[] paramTypes = method.getParameterTypes(); this.varargsPosition = paramTypes.length - 1; @@ -62,29 +70,33 @@ public class ReflectiveMethodExecutor implements MethodExecutor { } - public Method getMethod() { - return this.method; + /** + * Return the original method that this executor has been configured for. + */ + public final Method getMethod() { + return this.originalMethod; } /** * Find the first public class in the methods declaring class hierarchy that declares this method. * Sometimes the reflective method discovery logic finds a suitable method that can easily be * called via reflection but cannot be called from generated code when compiling the expression - * because of visibility restrictions. For example if a non public class overrides toString(), this - * helper method will walk up the type hierarchy to find the first public type that declares the - * method (if there is one!). For toString() it may walk as far as Object. + * because of visibility restrictions. For example if a non-public class overrides toString(), + * this helper method will walk up the type hierarchy to find the first public type that declares + * the method (if there is one!). For toString() it may walk as far as Object. */ @Nullable public Class getPublicDeclaringClass() { if (!this.computedPublicDeclaringClass) { - this.publicDeclaringClass = discoverPublicClass(this.method, this.method.getDeclaringClass()); + this.publicDeclaringClass = + discoverPublicDeclaringClass(this.originalMethod, this.originalMethod.getDeclaringClass()); this.computedPublicDeclaringClass = true; } return this.publicDeclaringClass; } @Nullable - private Class discoverPublicClass(Method method, Class clazz) { + private Class discoverPublicDeclaringClass(Method method, Class clazz) { if (Modifier.isPublic(clazz.getModifiers())) { try { clazz.getDeclaredMethod(method.getName(), method.getParameterTypes()); @@ -94,12 +106,8 @@ public class ReflectiveMethodExecutor implements MethodExecutor { // Continue below... } } - Class[] ifcs = clazz.getInterfaces(); - for (Class ifc: ifcs) { - discoverPublicClass(method, ifc); - } if (clazz.getSuperclass() != null) { - return discoverPublicClass(method, clazz.getSuperclass()); + return discoverPublicDeclaringClass(method, clazz.getSuperclass()); } return null; } @@ -113,17 +121,17 @@ public class ReflectiveMethodExecutor implements MethodExecutor { public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException { try { this.argumentConversionOccurred = ReflectionHelper.convertArguments( - context.getTypeConverter(), arguments, this.method, this.varargsPosition); - if (this.method.isVarArgs()) { + context.getTypeConverter(), arguments, this.originalMethod, this.varargsPosition); + if (this.originalMethod.isVarArgs()) { arguments = ReflectionHelper.setupArgumentsForVarargsInvocation( - this.method.getParameterTypes(), arguments); + this.originalMethod.getParameterTypes(), arguments); } - ReflectionUtils.makeAccessible(this.method); - Object value = this.method.invoke(target, arguments); - return new TypedValue(value, new TypeDescriptor(new MethodParameter(this.method, -1)).narrow(value)); + ReflectionUtils.makeAccessible(this.methodToInvoke); + Object value = this.methodToInvoke.invoke(target, arguments); + return new TypedValue(value, new TypeDescriptor(new MethodParameter(this.originalMethod, -1)).narrow(value)); } catch (Exception ex) { - throw new AccessException("Problem invoking method: " + this.method, ex); + throw new AccessException("Problem invoking method: " + this.methodToInvoke, ex); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java index 060e67e5c18..2aaf0cb171a 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/CachedMethodExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2018 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. @@ -16,7 +16,6 @@ package org.springframework.expression.spel; -import org.junit.Before; import org.junit.Test; import org.springframework.expression.Expression; @@ -36,17 +35,11 @@ public class CachedMethodExecutorTests { private final ExpressionParser parser = new SpelExpressionParser(); - private StandardEvaluationContext context; - - - @Before - public void setUp() throws Exception { - this.context = new StandardEvaluationContext(new RootObject()); - } + private final StandardEvaluationContext context = new StandardEvaluationContext(new RootObject()); @Test - public void testCachedExecutionForParameters() throws Exception { + public void testCachedExecutionForParameters() { Expression expression = this.parser.parseExpression("echo(#var)"); assertMethodExecution(expression, 42, "int: 42"); @@ -56,7 +49,7 @@ public class CachedMethodExecutorTests { } @Test - public void testCachedExecutionForTarget() throws Exception { + public void testCachedExecutionForTarget() { Expression expression = this.parser.parseExpression("#var.echo(42)"); assertMethodExecution(expression, new RootObject(), "int: 42"); @@ -76,7 +69,6 @@ public class CachedMethodExecutorTests { public String echo(String value) { return "String: " + value; } - } public static class RootObject extends BaseObject { @@ -84,7 +76,6 @@ public class CachedMethodExecutorTests { public String echo(int value) { return "int: " + value; } - } }