Skip to content
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

Closed
drekbour opened this issue Jan 19, 2023 · 6 comments
Closed
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Milestone

Comments

@drekbour
Copy link

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

   SpelExpressionParser parser = new SpelExpressionParser();
   StandardEvaluationContext context = new StandardEvaluationContext();

    @Test
    public void basic_expressions_are_compiled() {
        SpelExpression exp = (SpelExpression)parser.parseExpression("{2021, 2022}.contains(year)");
        assert exp.getValue(context, LocalDate.parse("2022-01-19")) == true;
        assert exp.compileExpression(); // FAIL - not compiled
    }

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 interface
public 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).

    public static class Y { public int getI() {return 1;} }
    private static class Z extends Y { public int getI() {return 2;} }

    @Test
    public void basic_expressions_are_compiled() {       
        SpelExpression exp = (SpelExpression)parser.parseExpression("i");
        Y y = new Z();
        assert exp.getValue(context, y) == 2;
        assert exp.compileExpression(); // FAIL - not compiled
    }

This is because OptimalPropertyAccessor.isCompilable() relies on Method.getDeclaringClass() which has this behaviour

public boolean isCompilable() {
	return (Modifier.isPublic(this.member.getModifiers()) &&
			Modifier.isPublic(this.member.getDeclaringClass().getModifiers()));
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jan 19, 2023
@jhoeller jhoeller added in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Jan 20, 2023
@jhoeller jhoeller added this to the 6.0.x milestone Jan 20, 2023
@jhoeller jhoeller self-assigned this Feb 8, 2023
@jhoeller jhoeller modified the milestones: 6.0.x, 6.0.5, 6.0.6 Feb 8, 2023
@jhoeller jhoeller modified the milestones: 6.0.6, 6.0.7 Feb 28, 2023
@jhoeller jhoeller modified the milestones: 6.0.7, 6.1.0-M1 Mar 8, 2023
@jhoeller jhoeller modified the milestones: 6.1.0-M1, 6.1.x May 8, 2023
@jhoeller jhoeller removed their assignment Jul 9, 2023
@bclozel bclozel modified the milestones: 6.1.x, 6.2.x Dec 18, 2023
@sbrannen sbrannen changed the title Basic SpEL expressions not compilable due to ignoring interfaces/parents method-owners Basic SpEL expressions not compilable due to ignoring interface/superclass as method owner Mar 3, 2024
@sbrannen
Copy link
Member

sbrannen commented Mar 3, 2024

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 ClassUtils.getInterfaceMethodIfPossible() and ReflectiveMethodExecutor.discoverPublicDeclaringClass() in SpEL, and I was wondering if we shouldn't rather use some sort of combination of the two.

As you pointed out, we may need to revisit MethodReference.isCompilable() and OptimalPropertyAccessor.isCompilable() as well.

In any case, we'll investigate our options.

Cheers,

Sam

@sbrannen
Copy link
Member

sbrannen commented Mar 3, 2024

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 new java.util.ArrayList({2021, 2022}).contains(2022) allows it to pass (since ArrayList is a public type).

Making PrivateSubclass public allows the second test to pass.

@sbrannen sbrannen self-assigned this Mar 3, 2024
@sbrannen sbrannen modified the milestones: 6.2.x, 6.2.0-M1 Mar 3, 2024
@sbrannen
Copy link
Member

sbrannen commented Mar 3, 2024

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

@sbrannen sbrannen changed the title Basic SpEL expressions not compilable due to ignoring interface/superclass as method owner Ensure methhods are invoked via public interface/superclass in compiled SpEL expressions Mar 5, 2024
@sbrannen sbrannen changed the title Ensure methhods are invoked via public interface/superclass in compiled SpEL expressions Ensure methods are invoked via public interface/superclass in compiled SpEL expressions Mar 5, 2024
@sbrannen sbrannen changed the title Ensure methods are invoked via public interface/superclass in compiled SpEL expressions Invoke methods via public interface/superclass in compiled SpEL expressions Mar 5, 2024
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 6, 2024
This commit ensures that methods are invoked via a public interface or
public superclass whenever possible when compiling SpEL expressions.

See spring-projectsgh-29857
sbrannen added a commit that referenced this issue Mar 7, 2024
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
@sbrannen
Copy link
Member

sbrannen commented Mar 7, 2024

Closed via c79436f

@sbrannen sbrannen closed this as completed Mar 7, 2024
@sbrannen
Copy link
Member

sbrannen commented Mar 9, 2024

While working on #29847, I noticed that similar issues exist when accessing a property by indexing into an object in Indexer.

In light of that, I am reopening this issue to address those issues as well.

@sbrannen sbrannen reopened this Mar 9, 2024
sbrannen added a commit that referenced this issue Mar 9, 2024
@sbrannen
Copy link
Member

sbrannen commented Mar 9, 2024

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 6.2.0 snapshots.

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

sbrannen added a commit that referenced this issue Aug 22, 2024
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

5 participants