Spring Expression Language (SpEL) provides a powerful expression language for querying and manipulating object graphs at runtime.
—
This document covers SpEL's method and constructor resolution capabilities, including resolver interfaces, standard implementations, and custom resolver development.
@FunctionalInterface
public interface MethodResolver {
MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException;
}{ .api }
public interface ConstructorResolver {
ConstructorExecutor resolve(EvaluationContext context, String typeName,
List<TypeDescriptor> argumentTypes) throws AccessException;
}{ .api }
public interface MethodExecutor {
TypedValue execute(EvaluationContext context, Object target, Object... arguments)
throws AccessException;
}
public interface ConstructorExecutor {
TypedValue execute(EvaluationContext context, Object... arguments) throws AccessException;
}{ .api }
public class ReflectiveMethodResolver implements MethodResolver {
public ReflectiveMethodResolver();
public ReflectiveMethodResolver(boolean useDistance);
public void registerMethodFilter(Class<?> type, MethodFilter filter);
@Override
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException;
}{ .api }
The default method resolver that uses Java reflection to find and invoke methods.
public class DataBindingMethodResolver implements MethodResolver {
@Override
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException;
}{ .api }
Optimized method resolver for data-binding scenarios with restricted method access.
public class Calculator {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public String concatenate(String... parts) {
return String.join("", parts);
}
public static int multiply(int a, int b) {
return a * b;
}
}
StandardEvaluationContext context = new StandardEvaluationContext();
context.addMethodResolver(new ReflectiveMethodResolver());
Calculator calc = new Calculator();
ExpressionParser parser = new SpelExpressionParser();
// Instance method invocation
Expression exp = parser.parseExpression("add(5, 3)");
Integer result = exp.getValue(context, calc, Integer.class); // 8
// Method overloading resolution
exp = parser.parseExpression("add(5.5, 3.2)");
Double doubleResult = exp.getValue(context, calc, Double.class); // 8.7
// Varargs method invocation
exp = parser.parseExpression("concatenate('Hello', ' ', 'World')");
String text = exp.getValue(context, calc, String.class); // "Hello World"
// Static method invocation (requires type reference)
exp = parser.parseExpression("T(Calculator).multiply(4, 6)");
Integer product = exp.getValue(context, Integer.class); // 24{ .api }
public class RestrictedMethodResolver extends ReflectiveMethodResolver {
public RestrictedMethodResolver() {
super();
// Register method filters for security
registerMethodFilter(Object.class, new SafeMethodFilter());
registerMethodFilter(String.class, new StringMethodFilter());
}
}
// Filter that allows only safe Object methods
public class SafeMethodFilter implements MethodFilter {
private final Set<String> allowedMethods = Set.of(
"toString", "hashCode", "equals"
);
@Override
public List<Method> filter(List<Method> methods) {
return methods.stream()
.filter(method -> allowedMethods.contains(method.getName()))
.collect(Collectors.toList());
}
}
// Filter that restricts String methods
public class StringMethodFilter implements MethodFilter {
private final Set<String> allowedMethods = Set.of(
"length", "toUpperCase", "toLowerCase", "trim", "substring",
"indexOf", "startsWith", "endsWith", "contains"
);
@Override
public List<Method> filter(List<Method> methods) {
return methods.stream()
.filter(method -> allowedMethods.contains(method.getName()))
.collect(Collectors.toList());
}
}
// Usage
StandardEvaluationContext context = new StandardEvaluationContext();
context.addMethodResolver(new RestrictedMethodResolver());
String text = "Hello World";
ExpressionParser parser = new SpelExpressionParser();
// Allowed methods work
Expression exp = parser.parseExpression("length()");
Integer length = exp.getValue(context, text, Integer.class); // 11
exp = parser.parseExpression("toUpperCase()");
String upper = exp.getValue(context, text, String.class); // "HELLO WORLD"
// Restricted methods would fail resolution
// exp = parser.parseExpression("getClass()"); // Would fail{ .api }
public class ReflectiveConstructorResolver implements ConstructorResolver {
@Override
public ConstructorExecutor resolve(EvaluationContext context, String typeName,
List<TypeDescriptor> argumentTypes) throws AccessException;
}{ .api }
StandardEvaluationContext context = new StandardEvaluationContext();
context.addConstructorResolver(new ReflectiveConstructorResolver());
ExpressionParser parser = new SpelExpressionParser();
// Constructor invocation with arguments
Expression exp = parser.parseExpression("new String('Hello World')");
String result = exp.getValue(context, String.class); // "Hello World"
// Default constructor
exp = parser.parseExpression("new java.util.ArrayList()");
ArrayList<?> list = exp.getValue(context, ArrayList.class); // empty list
// Constructor with multiple arguments
exp = parser.parseExpression("new java.util.Date(2023, 0, 1)");
Date date = exp.getValue(context, Date.class);
// Constructor chaining with method calls
exp = parser.parseExpression("new StringBuilder('Hello').append(' World').toString()");
String text = exp.getValue(context, String.class); // "Hello World"{ .api }
public class ReflectiveMethodExecutor implements MethodExecutor {
public ReflectiveMethodExecutor(Method method);
@Override
public TypedValue execute(EvaluationContext context, Object target, Object... arguments)
throws AccessException;
public Method getMethod();
}{ .api }
public class ReflectiveConstructorExecutor implements ConstructorExecutor {
public ReflectiveConstructorExecutor(Constructor<?> constructor);
@Override
public TypedValue execute(EvaluationContext context, Object... arguments)
throws AccessException;
public Constructor<?> getConstructor();
}{ .api }
public class CachingMethodResolver implements MethodResolver {
private final MethodResolver delegate;
private final Map<MethodKey, MethodExecutor> cache = new ConcurrentHashMap<>();
public CachingMethodResolver(MethodResolver delegate) {
this.delegate = delegate;
}
@Override
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException {
MethodKey key = new MethodKey(targetObject.getClass(), name, argumentTypes);
return cache.computeIfAbsent(key, k -> {
try {
return delegate.resolve(context, targetObject, name, argumentTypes);
} catch (AccessException e) {
return null; // Cache null results to avoid repeated failures
}
});
}
private static class MethodKey {
private final Class<?> targetClass;
private final String methodName;
private final List<Class<?>> argumentTypes;
public MethodKey(Class<?> targetClass, String methodName, List<TypeDescriptor> argTypes) {
this.targetClass = targetClass;
this.methodName = methodName;
this.argumentTypes = argTypes.stream()
.map(TypeDescriptor::getType)
.collect(Collectors.toList());
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof MethodKey)) return false;
MethodKey other = (MethodKey) obj;
return Objects.equals(targetClass, other.targetClass) &&
Objects.equals(methodName, other.methodName) &&
Objects.equals(argumentTypes, other.argumentTypes);
}
@Override
public int hashCode() {
return Objects.hash(targetClass, methodName, argumentTypes);
}
}
}{ .api }
public class FunctionRegistryResolver implements MethodResolver {
private final Map<String, Method> functions = new HashMap<>();
public void registerFunction(String name, Method method) {
functions.put(name, method);
}
public void registerFunction(String name, Class<?> clazz, String methodName,
Class<?>... parameterTypes) throws NoSuchMethodException {
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
functions.put(name, method);
}
@Override
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException {
Method function = functions.get(name);
if (function != null && isCompatible(function, argumentTypes)) {
return new FunctionExecutor(function);
}
return null; // Let other resolvers handle it
}
private boolean isCompatible(Method method, List<TypeDescriptor> argumentTypes) {
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length != argumentTypes.size()) {
return false;
}
for (int i = 0; i < paramTypes.length; i++) {
if (!paramTypes[i].isAssignableFrom(argumentTypes.get(i).getType())) {
return false;
}
}
return true;
}
private static class FunctionExecutor implements MethodExecutor {
private final Method method;
public FunctionExecutor(Method method) {
this.method = method;
}
@Override
public TypedValue execute(EvaluationContext context, Object target, Object... arguments)
throws AccessException {
try {
Object result = method.invoke(null, arguments); // Static invocation
return new TypedValue(result);
} catch (Exception e) {
throw new AccessException("Function execution failed", e);
}
}
}
}
// Usage
public class MathFunctions {
public static double sqrt(double value) {
return Math.sqrt(value);
}
public static long factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
public static boolean isPrime(int n) {
if (n < 2) return false;
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0) return false;
}
return true;
}
}
FunctionRegistryResolver functionResolver = new FunctionRegistryResolver();
functionResolver.registerFunction("sqrt", MathFunctions.class, "sqrt", double.class);
functionResolver.registerFunction("factorial", MathFunctions.class, "factorial", int.class);
functionResolver.registerFunction("isPrime", MathFunctions.class, "isPrime", int.class);
StandardEvaluationContext context = new StandardEvaluationContext();
context.addMethodResolver(functionResolver);
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("sqrt(16)");
Double result = exp.getValue(context, Double.class); // 4.0
exp = parser.parseExpression("factorial(5)");
Long factorial = exp.getValue(context, Long.class); // 120
exp = parser.parseExpression("isPrime(17)");
Boolean prime = exp.getValue(context, Boolean.class); // true{ .api }
public class DomainSpecificResolver implements MethodResolver {
private final Map<Class<?>, DomainHandler> domainHandlers = new HashMap<>();
public void registerDomainHandler(Class<?> domainClass, DomainHandler handler) {
domainHandlers.put(domainClass, handler);
}
@Override
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException {
DomainHandler handler = domainHandlers.get(targetObject.getClass());
if (handler != null && handler.canHandle(name, argumentTypes)) {
return new DomainMethodExecutor(handler, name);
}
return null;
}
public interface DomainHandler {
boolean canHandle(String methodName, List<TypeDescriptor> argumentTypes);
Object execute(String methodName, Object target, Object... arguments) throws Exception;
}
private static class DomainMethodExecutor implements MethodExecutor {
private final DomainHandler handler;
private final String methodName;
public DomainMethodExecutor(DomainHandler handler, String methodName) {
this.handler = handler;
this.methodName = methodName;
}
@Override
public TypedValue execute(EvaluationContext context, Object target, Object... arguments)
throws AccessException {
try {
Object result = handler.execute(methodName, target, arguments);
return new TypedValue(result);
} catch (Exception e) {
throw new AccessException("Domain method execution failed", e);
}
}
}
}
// Domain-specific handler for business objects
public class BusinessObjectHandler implements DomainSpecificResolver.DomainHandler {
@Override
public boolean canHandle(String methodName, List<TypeDescriptor> argumentTypes) {
return methodName.startsWith("calculate") ||
methodName.startsWith("validate") ||
methodName.startsWith("transform");
}
@Override
public Object execute(String methodName, Object target, Object... arguments) throws Exception {
BusinessObject bo = (BusinessObject) target;
return switch (methodName) {
case "calculateTotal" -> bo.getItems().stream()
.mapToDouble(Item::getPrice)
.sum();
case "validateRequired" -> bo.getName() != null && !bo.getName().isEmpty();
case "transformToUpper" -> bo.getName().toUpperCase();
default -> throw new UnsupportedOperationException("Unknown method: " + methodName);
};
}
}
// Usage
DomainSpecificResolver domainResolver = new DomainSpecificResolver();
domainResolver.registerDomainHandler(BusinessObject.class, new BusinessObjectHandler());
StandardEvaluationContext context = new StandardEvaluationContext();
context.addMethodResolver(domainResolver);
BusinessObject bo = new BusinessObject("Product");
bo.addItem(new Item("Item1", 10.0));
bo.addItem(new Item("Item2", 20.0));
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("calculateTotal()");
Double total = exp.getValue(context, bo, Double.class); // 30.0
exp = parser.parseExpression("validateRequired()");
Boolean valid = exp.getValue(context, bo, Boolean.class); // true
exp = parser.parseExpression("transformToUpper()");
String upper = exp.getValue(context, bo, String.class); // "PRODUCT"{ .api }
public class FactoryConstructorResolver implements ConstructorResolver {
private final Map<String, ObjectFactory<?>> factories = new HashMap<>();
public void registerFactory(String typeName, ObjectFactory<?> factory) {
factories.put(typeName, factory);
}
@Override
public ConstructorExecutor resolve(EvaluationContext context, String typeName,
List<TypeDescriptor> argumentTypes) throws AccessException {
ObjectFactory<?> factory = factories.get(typeName);
if (factory != null) {
return new FactoryExecutor(factory);
}
return null;
}
@FunctionalInterface
public interface ObjectFactory<T> {
T create(Object... arguments) throws Exception;
}
private static class FactoryExecutor implements ConstructorExecutor {
private final ObjectFactory<?> factory;
public FactoryExecutor(ObjectFactory<?> factory) {
this.factory = factory;
}
@Override
public TypedValue execute(EvaluationContext context, Object... arguments)
throws AccessException {
try {
Object result = factory.create(arguments);
return new TypedValue(result);
} catch (Exception e) {
throw new AccessException("Factory construction failed", e);
}
}
}
}
// Usage
FactoryConstructorResolver factoryResolver = new FactoryConstructorResolver();
// Register custom object factories
factoryResolver.registerFactory("Person", args -> {
if (args.length == 2) {
return new Person((String) args[0], (Integer) args[1]);
}
throw new IllegalArgumentException("Person requires name and age");
});
factoryResolver.registerFactory("Product", args -> {
if (args.length == 1) {
return new Product((String) args[0]);
}
throw new IllegalArgumentException("Product requires name");
});
StandardEvaluationContext context = new StandardEvaluationContext();
context.addConstructorResolver(factoryResolver);
ExpressionParser parser = new SpelExpressionParser();
// Use factory to create objects
Expression exp = parser.parseExpression("new Person('John', 30)");
Person person = exp.getValue(context, Person.class);
exp = parser.parseExpression("new Product('Widget')");
Product product = exp.getValue(context, Product.class);{ .api }
public class ChainableMethodResolver implements MethodResolver {
private final List<MethodResolver> resolvers = new ArrayList<>();
public ChainableMethodResolver addResolver(MethodResolver resolver) {
resolvers.add(resolver);
return this;
}
@Override
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException {
for (MethodResolver resolver : resolvers) {
try {
MethodExecutor executor = resolver.resolve(context, targetObject, name, argumentTypes);
if (executor != null) {
return executor;
}
} catch (AccessException e) {
// Continue to next resolver
}
}
return null; // No resolver could handle the method
}
}
// Usage
ChainableMethodResolver chainResolver = new ChainableMethodResolver()
.addResolver(new CachingMethodResolver(new ReflectiveMethodResolver()))
.addResolver(new FunctionRegistryResolver())
.addResolver(new DomainSpecificResolver());
StandardEvaluationContext context = new StandardEvaluationContext();
context.setMethodResolvers(Arrays.asList(chainResolver));{ .api }
public class ConditionalMethodResolver implements MethodResolver {
private final Predicate<ResolutionContext> condition;
private final MethodResolver resolver;
public ConditionalMethodResolver(Predicate<ResolutionContext> condition,
MethodResolver resolver) {
this.condition = condition;
this.resolver = resolver;
}
@Override
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException {
ResolutionContext resContext = new ResolutionContext(context, targetObject, name, argumentTypes);
if (condition.test(resContext)) {
return resolver.resolve(context, targetObject, name, argumentTypes);
}
return null;
}
public static class ResolutionContext {
private final EvaluationContext evaluationContext;
private final Object targetObject;
private final String methodName;
private final List<TypeDescriptor> argumentTypes;
public ResolutionContext(EvaluationContext evaluationContext, Object targetObject,
String methodName, List<TypeDescriptor> argumentTypes) {
this.evaluationContext = evaluationContext;
this.targetObject = targetObject;
this.methodName = methodName;
this.argumentTypes = argumentTypes;
}
// Getters...
public EvaluationContext getEvaluationContext() { return evaluationContext; }
public Object getTargetObject() { return targetObject; }
public String getMethodName() { return methodName; }
public List<TypeDescriptor> getArgumentTypes() { return argumentTypes; }
}
}
// Usage examples
MethodResolver secureResolver = new ConditionalMethodResolver(
ctx -> isSecureContext(ctx.getEvaluationContext()),
new RestrictedMethodResolver()
);
MethodResolver developmentResolver = new ConditionalMethodResolver(
ctx -> isDevelopmentMode(),
new ReflectiveMethodResolver()
);
private static boolean isSecureContext(EvaluationContext context) {
return context instanceof SimpleEvaluationContext;
}
private static boolean isDevelopmentMode() {
return "development".equals(System.getProperty("environment"));
}{ .api }
public class MetricsMethodResolver implements MethodResolver {
private final MethodResolver delegate;
private final Map<String, AtomicLong> resolutionCounts = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> resolutionTimes = new ConcurrentHashMap<>();
public MetricsMethodResolver(MethodResolver delegate) {
this.delegate = delegate;
}
@Override
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException {
String key = targetObject.getClass().getSimpleName() + "." + name;
long startTime = System.nanoTime();
try {
MethodExecutor executor = delegate.resolve(context, targetObject, name, argumentTypes);
if (executor != null) {
return new MetricsMethodExecutor(executor, key);
}
return null;
} finally {
long duration = System.nanoTime() - startTime;
resolutionCounts.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();
resolutionTimes.computeIfAbsent(key, k -> new AtomicLong()).addAndGet(duration);
}
}
public void printMetrics() {
System.out.println("Method Resolution Metrics:");
resolutionCounts.forEach((method, count) -> {
long totalTime = resolutionTimes.get(method).get();
long avgTime = totalTime / count.get();
System.out.printf("%s: %d calls, avg %d ns%n", method, count.get(), avgTime);
});
}
private static class MetricsMethodExecutor implements MethodExecutor {
private final MethodExecutor delegate;
private final String methodKey;
public MetricsMethodExecutor(MethodExecutor delegate, String methodKey) {
this.delegate = delegate;
this.methodKey = methodKey;
}
@Override
public TypedValue execute(EvaluationContext context, Object target, Object... arguments)
throws AccessException {
// Could add execution metrics here as well
return delegate.execute(context, target, arguments);
}
}
}{ .api }
// 1. Use method filtering for security
public class SecurityAwareMethodResolver extends ReflectiveMethodResolver {
public SecurityAwareMethodResolver() {
super();
// Register security filters for dangerous classes
registerMethodFilter(Runtime.class, methods -> Collections.emptyList());
registerMethodFilter(ProcessBuilder.class, methods -> Collections.emptyList());
registerMethodFilter(System.class, this::filterSystemMethods);
registerMethodFilter(Class.class, this::filterClassMethods);
}
private List<Method> filterSystemMethods(List<Method> methods) {
// Only allow safe System methods
Set<String> allowedMethods = Set.of("currentTimeMillis", "nanoTime");
return methods.stream()
.filter(m -> allowedMethods.contains(m.getName()))
.collect(Collectors.toList());
}
private List<Method> filterClassMethods(List<Method> methods) {
// Prevent reflection access
return Collections.emptyList();
}
}
// 2. Validate method arguments
public class ValidatingMethodExecutor implements MethodExecutor {
private final MethodExecutor delegate;
@Override
public TypedValue execute(EvaluationContext context, Object target, Object... arguments)
throws AccessException {
validateArguments(arguments);
return delegate.execute(context, target, arguments);
}
private void validateArguments(Object[] arguments) throws AccessException {
for (Object arg : arguments) {
if (arg instanceof String) {
String str = (String) arg;
if (str.length() > 1000) { // Prevent extremely large strings
throw new AccessException("String argument too large");
}
}
}
}
}{ .api }
CompilablePropertyAccessor for hot paths where possiblepublic class RobustMethodResolver implements MethodResolver {
private final MethodResolver delegate;
private final boolean logErrors;
public RobustMethodResolver(MethodResolver delegate, boolean logErrors) {
this.delegate = delegate;
this.logErrors = logErrors;
}
@Override
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException {
try {
return delegate.resolve(context, targetObject, name, argumentTypes);
} catch (AccessException e) {
if (logErrors) {
System.err.printf("Method resolution failed for %s.%s: %s%n",
targetObject.getClass().getSimpleName(), name, e.getMessage());
}
return null; // Let other resolvers try
} catch (Exception e) {
if (logErrors) {
System.err.printf("Unexpected error resolving %s.%s: %s%n",
targetObject.getClass().getSimpleName(), name, e.getMessage());
}
return null;
}
}
}{ .api }
Install with Tessl CLI
npx tessl i tessl/maven-org-springframework--spring-expression