-
Notifications
You must be signed in to change notification settings - Fork 38.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Invoke methods via public interface/superclass in compiled SpEL expressions #29857
Comments
Hi @drekbour, Congratulations on submitting your first issue for the Spring Framework! 👍 And thanks for the detailed write-up. It turns out that I recently had similar questions about our mixed use of As you pointed out, we may need to revisit In any case, we'll investigate our options. Cheers, Sam |
To simplify matters for whoever investigates this further, I've combined the two provided test cases into a single test class. package example;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.ast.InlineList;
import org.springframework.expression.spel.standard.SpelCompiler;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import static org.assertj.core.api.Assertions.assertThat;
class SpelPrivateTypesTests {
private final SpelExpressionParser parser = new SpelExpressionParser();
private final StandardEvaluationContext context = new StandardEvaluationContext();
private Expression expression;
/**
* Note that {@link InlineList} creates a list and wraps it via
* {@link Collections#unmodifiableList(List)}, whose concrete type is private.
*/
@Test
void privateSubclassOverridesMethodInPublicInterface() {
expression = parser.parseExpression("{2021, 2022}");
List<?> inlineList = expression.getValue(List.class);
assertThat(Modifier.isPublic(inlineList.getClass().getModifiers())).isFalse();
expression = parser.parseExpression("{2021, 2022}.contains(2022)");
Boolean result = expression.getValue(context, Boolean.class);
assertThat(result).isTrue();
assertCanCompile(expression);
result = expression.getValue(context, Boolean.class);
assertThat(result).isTrue();
}
@Test
void privateSubclassOverridesMethodInPublicSuperclass() {
expression = parser.parseExpression("number");
PublicSuperclass privateSubclass = new PrivateSubclass();
Integer result = expression.getValue(context, privateSubclass, Integer.class);
assertThat(result).isEqualTo(2);
assertCanCompile(expression);
result = expression.getValue(context, privateSubclass, Integer.class);
assertThat(result).isEqualTo(2);
}
private static void assertCanCompile(Expression expression) {
assertThat(SpelCompiler.compile(expression))
.as(() -> "Expression <%s> should be compilable"
.formatted(((SpelExpression) expression).toStringAST()))
.isTrue();
}
public static class PublicSuperclass {
public int getNumber() {
return 1;
}
}
private static class PrivateSubclass extends PublicSuperclass {
@Override
public int getNumber() {
return 2;
}
}
} Changing the expression in the first test to Making |
Current work on this issue can be viewed in the following feature branch. main...sbrannen:spring-framework:issues/gh-29857-SpEL-compilation-public-methods-in-private-subtypes |
This commit ensures that methods are invoked via a public interface or public superclass whenever possible when compiling SpEL expressions. See spring-projectsgh-29857
Although the Spring Expression Language (SpEL) generally does a good job of locating the public declaring class or interface on which to invoke a method in a compiled expression, prior to this commit there were still a few unsupported use cases. To address those remaining use cases, this commit ensures that methods are invoked via a public interface or public superclass whenever possible when compiling SpEL expressions. See gh-29857
Closed via c79436f |
While working on #29847, I noticed that similar issues exist when accessing a property by indexing into an object in In light of that, I am reopening this issue to address those issues as well. |
Work on this issue has been completed in c79436f, 65d7762, and f4c1ad7. @drekbour and anyone else interested, feel free to try out the changes in upcoming Spring Framework If you encounter any issues, please report those back here. Thanks, Sam Information on working with snapshot builds can be found here: https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Artifacts#snapshots |
Prior to this commit, when invoking init methods and destroy methods for beans as well as methods within Spring Expression Language (SpEL) expressions via reflection, we invoked them based on the "interface method" returned from ClassUtils.getInterfaceMethodIfPossible(). That works well for finding methods defined in an interface, but it does not find public methods defined in a public superclass. For example, in a SpEL expression it was previously impossible to invoke toString() on a non-public type from a different module. This could be seen when attempting to invoke toString() on an unmodifiable list created by Collections.unmodifiableList(...). Doing so resulted in an InaccessibleObjectException. Although users can address that by adding an appropriate --add-opens declaration, such as --add-opens java.base/java.util=ALL-UNNAMED, it is better if applications do not have to add an --add-opens declaration for such use cases in SpEL. The same applies to init methods and destroy methods for beans. This commit therefore introduces a new getPubliclyAccessibleMethodIfPossible() method in ClassUtils which serves as a replacement for getInterfaceMethodIfPossible(). This new method finds the first publicly accessible method in the supplied method's type hierarchy that has a method signature equivalent to the supplied method. If the supplied method is public and declared in a public type, the supplied method will be returned. Otherwise, this method recursively searches the class hierarchy and implemented interfaces for an equivalent method that is public and declared in a public type. If a publicly accessible equivalent method cannot be found, the supplied method will be returned, indicating that no such equivalent method exists. All usage of getInterfaceMethodIfPossible() has been replaced with getPubliclyAccessibleMethodIfPossible() in spring-beans and spring-expression. In addition, getInterfaceMethodIfPossible() has been marked as deprecated in favor of the new method. As a bonus, the introduction of getPubliclyAccessibleMethodIfPossible() allows us to delete a fair amount of obsolete code within the SpEL infrastructure. See gh-29857 Closes gh-33216
Affects: 6.0.x
Many categories of basic SpelExpression are not compiled into a performant form due to a pattern of not acknowledging a
Method
can be called from superclass or interface. Best described with two simple failing tests for two scenarios. I have raised the issue as this feels like something resolvable.ITEM1 MethodReference.isCompilable
In this case it is the
contains
...org.springframework.expression.spel.ast.MethodReference#isCompilable
... checks concrete class and fails because it is not
public
...public boolean java.util.Collections$UnmodifiableCollection.contains(java.lang.Object)
... even though
MethodExecutor
is already aware that the 'methodToInvoke' will be against an interfacepublic abstract boolean java.util.Collection.contains(java.lang.Object)
If the execution is on the interface, the check must be too.
Note: Similar expressions on properties (not methods) compile just fine (e.g.
{2021, 2022}.size
)ITEM2 OptimalPropertyAccessor.isCompilable
However
OptimalPropertyAccessor
doesn't seem to understand overridden methods of parent classes (not interfaces which works fine as above).This is because
OptimalPropertyAccessor.isCompilable()
relies onMethod.getDeclaringClass()
which has this behaviourThe text was updated successfully, but these errors were encountered: