Skip to content

Commit 96844a0

Browse files
committed
Support SpEL compilation for 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
1 parent 40da093 commit 96844a0

File tree

7 files changed

+357
-58
lines changed

7 files changed

+357
-58
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,10 @@ public boolean isCompilable() {
299299
return false;
300300
}
301301

302-
Class<?> clazz = executor.getMethod().getDeclaringClass();
303-
return (Modifier.isPublic(clazz.getModifiers()) || executor.getPublicDeclaringClass() != null);
302+
Method method = executor.getMethod();
303+
return ((Modifier.isPublic(method.getModifiers()) &&
304+
(Modifier.isPublic(method.getDeclaringClass().getModifiers()) ||
305+
executor.getPublicDeclaringClass() != null)));
304306
}
305307

306308
@Override
@@ -310,6 +312,18 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) {
310312
throw new IllegalStateException("No applicable cached executor found: " + executorToCheck);
311313
}
312314
Method method = methodExecutor.getMethod();
315+
316+
Class<?> publicDeclaringClass;
317+
if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
318+
publicDeclaringClass = method.getDeclaringClass();
319+
}
320+
else {
321+
publicDeclaringClass = methodExecutor.getPublicDeclaringClass();
322+
}
323+
Assert.state(publicDeclaringClass != null,
324+
() -> "Failed to find public declaring class for method: " + method);
325+
326+
String classDesc = publicDeclaringClass.getName().replace('.', '/');
313327
boolean isStatic = Modifier.isStatic(method.getModifiers());
314328
String descriptor = cf.lastDescriptor();
315329

@@ -339,24 +353,15 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) {
339353
CodeFlow.insertBoxIfNecessary(mv, descriptor.charAt(0));
340354
}
341355

342-
String classDesc;
343-
if (Modifier.isPublic(method.getDeclaringClass().getModifiers())) {
344-
classDesc = method.getDeclaringClass().getName().replace('.', '/');
345-
}
346-
else {
347-
Class<?> publicDeclaringClass = methodExecutor.getPublicDeclaringClass();
348-
Assert.state(publicDeclaringClass != null, "No public declaring class");
349-
classDesc = publicDeclaringClass.getName().replace('.', '/');
350-
}
351-
352356
if (!isStatic && (descriptor == null || !descriptor.substring(1).equals(classDesc))) {
353357
CodeFlow.insertCheckCast(mv, "L" + classDesc);
354358
}
355359

356360
generateCodeForArguments(mv, cf, method, this.children);
357-
int opcode = (isStatic ? INVOKESTATIC : method.isDefault() ? INVOKEINTERFACE : INVOKEVIRTUAL);
361+
boolean isInterface = publicDeclaringClass.isInterface();
362+
int opcode = (isStatic ? INVOKESTATIC : isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL);
358363
mv.visitMethodInsn(opcode, classDesc, method.getName(), CodeFlow.createSignatureDescriptor(method),
359-
method.getDeclaringClass().isInterface());
364+
isInterface);
360365
cf.pushDescriptor(this.exitTypeDescriptor);
361366

362367
if (this.originalPrimitiveExitTypeDescriptor != null) {

spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import java.lang.reflect.Array;
2222
import java.lang.reflect.Executable;
2323
import java.lang.reflect.Method;
24+
import java.lang.reflect.Modifier;
2425
import java.util.List;
26+
import java.util.Map;
2527
import java.util.Optional;
2628

2729
import org.springframework.core.MethodParameter;
@@ -34,6 +36,7 @@
3436
import org.springframework.util.Assert;
3537
import org.springframework.util.ClassUtils;
3638
import org.springframework.util.CollectionUtils;
39+
import org.springframework.util.ConcurrentReferenceHashMap;
3740
import org.springframework.util.MethodInvoker;
3841

3942
/**
@@ -47,6 +50,14 @@
4750
*/
4851
public abstract class ReflectionHelper {
4952

53+
/**
54+
* Cache for equivalent methods in a public declaring class in the type
55+
* hierarchy of the method's declaring class.
56+
* @since 6.2
57+
*/
58+
private static final Map<Method, Class<?>> publicDeclaringClassCache = new ConcurrentReferenceHashMap<>(256);
59+
60+
5061
/**
5162
* Compare argument arrays and return information about whether they match.
5263
* <p>A supplied type converter and conversionAllowed flag allow for matches to take
@@ -488,6 +499,66 @@ public static Object[] setupArgumentsForVarargsInvocation(Class<?>[] requiredPar
488499
return args;
489500
}
490501

502+
/**
503+
* Find the first public class or interface in the method's class hierarchy
504+
* that declares the supplied method.
505+
* <p>Sometimes the reflective method discovery logic finds a suitable method
506+
* that can easily be called via reflection but cannot be called from generated
507+
* code when compiling the expression because of visibility restrictions. For
508+
* example, if a non-public class overrides {@code toString()}, this method
509+
* will traverse up the type hierarchy to find the first public type that
510+
* declares the method (if there is one). For {@code toString()}, it may
511+
* traverse as far as {@link Object}.
512+
* @param method the method to process
513+
* @return the public class or interface that declares the method, or
514+
* {@code null} if no such public type could be found
515+
* @since 6.2
516+
*/
517+
@Nullable
518+
public static Class<?> findPublicDeclaringClass(Method method) {
519+
return publicDeclaringClassCache.computeIfAbsent(method, key -> {
520+
// If the method is already defined in a public type, return that type.
521+
if (Modifier.isPublic(key.getDeclaringClass().getModifiers())) {
522+
return key.getDeclaringClass();
523+
}
524+
Method interfaceMethod = ClassUtils.getInterfaceMethodIfPossible(key, null);
525+
// If we found an interface method whose type is public, return the interface type.
526+
if (!interfaceMethod.equals(key)) {
527+
if (Modifier.isPublic(interfaceMethod.getDeclaringClass().getModifiers())) {
528+
return interfaceMethod.getDeclaringClass();
529+
}
530+
}
531+
// Attempt to search the type hierarchy.
532+
Class<?> superclass = key.getDeclaringClass().getSuperclass();
533+
if (superclass != null) {
534+
return findPublicDeclaringClass(superclass, key.getName(), key.getParameterTypes());
535+
}
536+
// Otherwise, no public declaring class found.
537+
return null;
538+
});
539+
}
540+
541+
@Nullable
542+
private static Class<?> findPublicDeclaringClass(
543+
Class<?> declaringClass, String methodName, Class<?>[] parameterTypes) {
544+
545+
if (Modifier.isPublic(declaringClass.getModifiers())) {
546+
try {
547+
declaringClass.getDeclaredMethod(methodName, parameterTypes);
548+
return declaringClass;
549+
}
550+
catch (NoSuchMethodException ex) {
551+
// Continue below...
552+
}
553+
}
554+
555+
Class<?> superclass = declaringClass.getSuperclass();
556+
if (superclass != null) {
557+
return findPublicDeclaringClass(superclass, methodName, parameterTypes);
558+
}
559+
return null;
560+
}
561+
491562

492563
/**
493564
* Arguments match kinds.

spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.expression.spel.support;
1818

1919
import java.lang.reflect.Method;
20-
import java.lang.reflect.Modifier;
2120

2221
import org.springframework.core.MethodParameter;
2322
import org.springframework.core.convert.TypeDescriptor;
@@ -34,6 +33,7 @@
3433
*
3534
* @author Andy Clement
3635
* @author Juergen Hoeller
36+
* @author Sam Brannen
3737
* @since 3.0
3838
*/
3939
public class ReflectiveMethodExecutor implements MethodExecutor {
@@ -91,43 +91,22 @@ public final Method getMethod() {
9191
}
9292

9393
/**
94-
* Find the first public class in the method's declaring class hierarchy that
95-
* declares this method.
96-
* <p>Sometimes the reflective method discovery logic finds a suitable method
97-
* that can easily be called via reflection but cannot be called from generated
98-
* code when compiling the expression because of visibility restrictions. For
99-
* example, if a non-public class overrides {@code toString()}, this helper
100-
* method will traverse up the type hierarchy to find the first public type that
101-
* declares the method (if there is one). For {@code toString()}, it may traverse
102-
* as far as Object.
94+
* Find a public class or interface in the method's class hierarchy that
95+
* declares the {@linkplain #getMethod() original method}.
96+
* <p>See {@link ReflectionHelper#findPublicDeclaringClass(Method)} for
97+
* details.
98+
* @return the public class or interface that declares the method, or
99+
* {@code null} if no such public type could be found
103100
*/
104101
@Nullable
105102
public Class<?> getPublicDeclaringClass() {
106103
if (!this.computedPublicDeclaringClass) {
107-
this.publicDeclaringClass =
108-
discoverPublicDeclaringClass(this.originalMethod, this.originalMethod.getDeclaringClass());
104+
this.publicDeclaringClass = ReflectionHelper.findPublicDeclaringClass(this.originalMethod);
109105
this.computedPublicDeclaringClass = true;
110106
}
111107
return this.publicDeclaringClass;
112108
}
113109

114-
@Nullable
115-
private Class<?> discoverPublicDeclaringClass(Method method, Class<?> clazz) {
116-
if (Modifier.isPublic(clazz.getModifiers())) {
117-
try {
118-
clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
119-
return clazz;
120-
}
121-
catch (NoSuchMethodException ex) {
122-
// Continue below...
123-
}
124-
}
125-
if (clazz.getSuperclass() != null) {
126-
return discoverPublicDeclaringClass(method, clazz.getSuperclass());
127-
}
128-
return null;
129-
}
130-
131110
public boolean didArgumentConversionOccur() {
132111
return this.argumentConversionOccurred;
133112
}

spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ public boolean canRead(EvaluationContext context, @Nullable Object target, Strin
136136
// The readerCache will only contain gettable properties (let's not worry about setters for now).
137137
Property property = new Property(type, method, null);
138138
TypeDescriptor typeDescriptor = new TypeDescriptor(property);
139-
method = ClassUtils.getInterfaceMethodIfPossible(method, type);
140-
this.readerCache.put(cacheKey, new InvokerPair(method, typeDescriptor));
139+
Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(method, type);
140+
this.readerCache.put(cacheKey, new InvokerPair(methodToInvoke, typeDescriptor, method));
141141
this.typeDescriptorCache.put(cacheKey, typeDescriptor);
142142
return true;
143143
}
@@ -171,22 +171,23 @@ public TypedValue read(EvaluationContext context, @Nullable Object target, Strin
171171

172172
if (invoker == null || invoker.member instanceof Method) {
173173
Method method = (Method) (invoker != null ? invoker.member : null);
174+
Method methodToInvoke = method;
174175
if (method == null) {
175176
method = findGetterForProperty(name, type, target);
176177
if (method != null) {
177178
// Treat it like a property...
178179
// The readerCache will only contain gettable properties (let's not worry about setters for now).
179180
Property property = new Property(type, method, null);
180181
TypeDescriptor typeDescriptor = new TypeDescriptor(property);
181-
method = ClassUtils.getInterfaceMethodIfPossible(method, type);
182-
invoker = new InvokerPair(method, typeDescriptor);
182+
methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(method, type);
183+
invoker = new InvokerPair(methodToInvoke, typeDescriptor, method);
183184
this.readerCache.put(cacheKey, invoker);
184185
}
185186
}
186-
if (method != null) {
187+
if (methodToInvoke != null) {
187188
try {
188-
ReflectionUtils.makeAccessible(method);
189-
Object value = method.invoke(target);
189+
ReflectionUtils.makeAccessible(methodToInvoke);
190+
Object value = methodToInvoke.invoke(target);
190191
return new TypedValue(value, invoker.typeDescriptor.narrow(value));
191192
}
192193
catch (Exception ex) {
@@ -532,9 +533,9 @@ public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullab
532533
method = findGetterForProperty(name, type, target);
533534
if (method != null) {
534535
TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(method, -1));
535-
method = ClassUtils.getInterfaceMethodIfPossible(method, type);
536-
invokerPair = new InvokerPair(method, typeDescriptor);
537-
ReflectionUtils.makeAccessible(method);
536+
Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(method, type);
537+
invokerPair = new InvokerPair(methodToInvoke, typeDescriptor, method);
538+
ReflectionUtils.makeAccessible(methodToInvoke);
538539
this.readerCache.put(cacheKey, invokerPair);
539540
}
540541
}
@@ -572,8 +573,14 @@ private static boolean isKotlinProperty(Method method, String methodSuffix) {
572573
/**
573574
* Captures the member (method/field) to call reflectively to access a property value
574575
* and the type descriptor for the value returned by the reflective call.
576+
* <p>The {@code originalMethod} is only used if the member is a method.
575577
*/
576-
private record InvokerPair(Member member, TypeDescriptor typeDescriptor) {}
578+
private record InvokerPair(Member member, TypeDescriptor typeDescriptor, @Nullable Method originalMethod) {
579+
580+
InvokerPair(Member member, TypeDescriptor typeDescriptor) {
581+
this(member, typeDescriptor, null);
582+
}
583+
}
577584

578585
private record PropertyCacheKey(Class<?> clazz, String property, boolean targetIsClass)
579586
implements Comparable<PropertyCacheKey> {
@@ -606,9 +613,13 @@ public static class OptimalPropertyAccessor implements CompilablePropertyAccesso
606613

607614
private final TypeDescriptor typeDescriptor;
608615

616+
@Nullable
617+
private final Method originalMethod;
618+
609619
OptimalPropertyAccessor(InvokerPair invokerPair) {
610620
this.member = invokerPair.member;
611621
this.typeDescriptor = invokerPair.typeDescriptor;
622+
this.originalMethod = invokerPair.originalMethod;
612623
}
613624

614625
@Override
@@ -677,8 +688,14 @@ public void write(EvaluationContext context, @Nullable Object target, String nam
677688

678689
@Override
679690
public boolean isCompilable() {
680-
return (Modifier.isPublic(this.member.getModifiers()) &&
681-
Modifier.isPublic(this.member.getDeclaringClass().getModifiers()));
691+
if (Modifier.isPublic(this.member.getModifiers()) &&
692+
Modifier.isPublic(this.member.getDeclaringClass().getModifiers())) {
693+
return true;
694+
}
695+
if (this.originalMethod != null) {
696+
return (ReflectionHelper.findPublicDeclaringClass(this.originalMethod) != null);
697+
}
698+
return false;
682699
}
683700

684701
@Override
@@ -693,9 +710,17 @@ public Class<?> getPropertyType() {
693710

694711
@Override
695712
public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) {
713+
Class<?> publicDeclaringClass = this.member.getDeclaringClass();
714+
if (this.originalMethod != null) {
715+
publicDeclaringClass = ReflectionHelper.findPublicDeclaringClass(this.originalMethod);
716+
}
717+
Assert.state(publicDeclaringClass != null && Modifier.isPublic(publicDeclaringClass.getModifiers()),
718+
() -> "Failed to find public declaring class for: " +
719+
(this.originalMethod != null ? this.originalMethod : this.member));
720+
721+
String classDesc = publicDeclaringClass.getName().replace('.', '/');
696722
boolean isStatic = Modifier.isStatic(this.member.getModifiers());
697723
String descriptor = cf.lastDescriptor();
698-
String classDesc = this.member.getDeclaringClass().getName().replace('.', '/');
699724

700725
if (!isStatic) {
701726
if (descriptor == null) {
@@ -714,7 +739,7 @@ public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) {
714739
}
715740

716741
if (this.member instanceof Method method) {
717-
boolean isInterface = method.getDeclaringClass().isInterface();
742+
boolean isInterface = publicDeclaringClass.isInterface();
718743
int opcode = (isStatic ? INVOKESTATIC : isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL);
719744
mv.visitMethodInsn(opcode, classDesc, method.getName(),
720745
CodeFlow.createSignatureDescriptor(method), isInterface);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.expression.spel;
18+
19+
/**
20+
* This is intentionally a top-level public interface.
21+
*/
22+
public interface PublicInterface {
23+
24+
String getText();
25+
26+
}

0 commit comments

Comments
 (0)