CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-springframework--spring-expression

Spring Expression Language (SpEL) provides a powerful expression language for querying and manipulating object graphs at runtime.

Pending
Overview
Eval results
Files

method-constructor-resolution.mddocs/

Method and Constructor Resolution

This document covers SpEL's method and constructor resolution capabilities, including resolver interfaces, standard implementations, and custom resolver development.

Core Resolution Interfaces

MethodResolver Interface

@FunctionalInterface
public interface MethodResolver {
    MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, 
                          List<TypeDescriptor> argumentTypes) throws AccessException;
}

{ .api }

ConstructorResolver Interface

public interface ConstructorResolver {
    ConstructorExecutor resolve(EvaluationContext context, String typeName, 
                               List<TypeDescriptor> argumentTypes) throws AccessException;
}

{ .api }

Executor Interfaces

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 }

Standard Method Resolution

ReflectiveMethodResolver Class

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.

DataBindingMethodResolver Class

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.

Method Resolution Examples

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 }

Method Filtering

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 }

Standard Constructor Resolution

ReflectiveConstructorResolver Class

public class ReflectiveConstructorResolver implements ConstructorResolver {
    @Override
    public ConstructorExecutor resolve(EvaluationContext context, String typeName, 
                                     List<TypeDescriptor> argumentTypes) throws AccessException;
}

{ .api }

Constructor Resolution Examples

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 }

Standard Executors

ReflectiveMethodExecutor Class

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 }

ReflectiveConstructorExecutor Class

public class ReflectiveConstructorExecutor implements ConstructorExecutor {
    public ReflectiveConstructorExecutor(Constructor<?> constructor);
    
    @Override
    public TypedValue execute(EvaluationContext context, Object... arguments) 
        throws AccessException;
    
    public Constructor<?> getConstructor();
}

{ .api }

Custom Method Resolvers

Cached Method Resolver

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 }

Function Registry Resolver

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 }

Domain-Specific Method Resolver

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 }

Custom Constructor Resolvers

Factory-based Constructor Resolver

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 }

Advanced Resolution Patterns

Chainable Method Resolver

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 }

Conditional Method Resolver

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 }

Performance Optimization

Method Resolution Metrics

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 }

Best Practices

Security Best Practices

// 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 }

Performance Tips

  1. Use method filtering: Filter out methods at resolution time rather than execution time
  2. Implement caching: Cache method resolution results for frequently called methods
  3. Order resolvers by frequency: Place most commonly used resolvers first
  4. Use conditional resolvers: Only invoke expensive resolvers when necessary
  5. Monitor resolution metrics: Track which methods are resolved most frequently
  6. Prefer compilation: Use CompilablePropertyAccessor for hot paths where possible

Error Handling Best Practices

public 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

docs

advanced-features.md

error-handling.md

evaluation-contexts.md

expression-evaluation.md

index.md

method-constructor-resolution.md

property-index-access.md

spel-configuration.md

type-system-support.md

tile.json