Support SpEL compilation for public methods in private subtypes

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
This commit is contained in:
Sam Brannen
2024-03-03 16:44:00 +01:00
parent 1ffffef85e
commit c79436f832
7 changed files with 357 additions and 58 deletions

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2002-2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.expression.spel;
/**
* This is intentionally a top-level public interface.
*/
public interface PublicInterface {
String getText();
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright 2002-2024 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.expression.spel;
/**
* This is intentionally a top-level public class.
*/
public class PublicSuperclass {
public int process(int num) {
return num + 1;
}
public int getNumber() {
return 1;
}
public String getMessage() {
return "goodbye";
}
public String greet(String name) {
return "Super, " + name;
}
}

View File

@@ -42,6 +42,7 @@ import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.ast.CompoundExpression;
import org.springframework.expression.spel.ast.InlineList;
import org.springframework.expression.spel.ast.OpLT;
import org.springframework.expression.spel.ast.SpelNodeImpl;
import org.springframework.expression.spel.ast.Ternary;
@@ -678,6 +679,158 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
}
@Nested
class PropertyVisibilityTests {
@Test
void privateSubclassOverridesPropertyInPublicInterface() {
expression = parser.parseExpression("text");
PrivateSubclass privateSubclass = new PrivateSubclass();
// Prerequisite: type must not be public for this use case.
assertNotPublic(privateSubclass.getClass());
String result = expression.getValue(context, privateSubclass, String.class);
assertThat(result).isEqualTo("enigma");
assertCanCompile(expression);
result = expression.getValue(context, privateSubclass, String.class);
assertThat(result).isEqualTo("enigma");
}
@Test
void privateSubclassOverridesPropertyInPrivateInterface() {
expression = parser.parseExpression("message");
PrivateSubclass privateSubclass = new PrivateSubclass();
// Prerequisite: type must not be public for this use case.
assertNotPublic(privateSubclass.getClass());
String result = expression.getValue(context, privateSubclass, String.class);
assertThat(result).isEqualTo("hello");
assertCanCompile(expression);
result = expression.getValue(context, privateSubclass, String.class);
assertThat(result).isEqualTo("hello");
}
@Test
void privateSubclassOverridesPropertyInPublicSuperclass() {
expression = parser.parseExpression("number");
PrivateSubclass privateSubclass = new PrivateSubclass();
// Prerequisite: type must not be public for this use case.
assertNotPublic(privateSubclass.getClass());
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 interface PrivateInterface {
String getMessage();
}
private static class PrivateSubclass extends PublicSuperclass implements PublicInterface, PrivateInterface {
@Override
public int getNumber() {
return 2;
}
@Override
public String getText() {
return "enigma";
}
@Override
public String getMessage() {
return "hello";
}
}
}
@Nested
class MethodVisibilityTests {
/**
* Note that {@link InlineList} creates a list and wraps it via
* {@link Collections#unmodifiableList(List)}, whose concrete type is
* package private.
*/
@Test
void packagePrivateSubclassOverridesMethodInPublicInterface() {
expression = parser.parseExpression("{2021, 2022}");
List<?> inlineList = expression.getValue(List.class);
// Prerequisite: type must not be public for this use case.
assertNotPublic(inlineList.getClass());
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 packagePrivateSubclassOverridesMethodInPrivateInterface() {
expression = parser.parseExpression("greet('Jane')");
PrivateSubclass privateSubclass = new PrivateSubclass();
// Prerequisite: type must not be public for this use case.
assertNotPublic(privateSubclass.getClass());
String result = expression.getValue(context, privateSubclass, String.class);
assertThat(result).isEqualTo("Hello, Jane");
assertCanCompile(expression);
result = expression.getValue(context, privateSubclass, String.class);
assertThat(result).isEqualTo("Hello, Jane");
}
@Test
void privateSubclassOverridesMethodInPublicSuperclass() {
expression = parser.parseExpression("process(2)");
PrivateSubclass privateSubclass = new PrivateSubclass();
// Prerequisite: type must not be public for this use case.
assertNotPublic(privateSubclass.getClass());
Integer result = expression.getValue(context, privateSubclass, Integer.class);
assertThat(result).isEqualTo(2 * 2);
assertCanCompile(expression);
result = expression.getValue(context, privateSubclass, Integer.class);
assertThat(result).isEqualTo(2 * 2);
}
private interface PrivateInterface {
String greet(String name);
}
private static class PrivateSubclass extends PublicSuperclass implements PrivateInterface {
@Override
public int process(int num) {
return num * 2;
}
@Override
public String greet(String name) {
return "Hello, " + name;
}
}
}
@Test
void typeReference() {
expression = parse("T(String)");