Spring Expression Language (SpEL) provides a powerful expression language for querying and manipulating object graphs at runtime.
—
This document covers SpEL's property and index access capabilities, including accessor interfaces, standard implementations, and custom accessor development.
public interface TargetedAccessor {
Class<?>[] getSpecificTargetClasses();
}{ .api }
The base interface for accessors that target specific classes for optimization.
public interface PropertyAccessor extends TargetedAccessor {
boolean canRead(EvaluationContext context, Object target, String name) throws AccessException;
TypedValue read(EvaluationContext context, Object target, String name) throws AccessException;
boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException;
void write(EvaluationContext context, Object target, String name, Object newValue)
throws AccessException;
}{ .api }
public interface IndexAccessor extends TargetedAccessor {
boolean canRead(EvaluationContext context, Object target, Object index) throws AccessException;
TypedValue read(EvaluationContext context, Object target, Object index) throws AccessException;
boolean canWrite(EvaluationContext context, Object target, Object index) throws AccessException;
void write(EvaluationContext context, Object target, Object index, Object newValue)
throws AccessException;
}{ .api }
public class ReflectivePropertyAccessor implements PropertyAccessor {
public ReflectivePropertyAccessor();
public ReflectivePropertyAccessor(boolean allowWrite);
@Override
public Class<?>[] getSpecificTargetClasses();
@Override
public boolean canRead(EvaluationContext context, Object target, String name)
throws AccessException;
@Override
public TypedValue read(EvaluationContext context, Object target, String name)
throws AccessException;
@Override
public boolean canWrite(EvaluationContext context, Object target, String name)
throws AccessException;
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue)
throws AccessException;
}{ .api }
The default property accessor that uses Java reflection to access object properties via getter/setter methods and fields.
public class DataBindingPropertyAccessor implements PropertyAccessor {
@Override
public Class<?>[] getSpecificTargetClasses();
@Override
public boolean canRead(EvaluationContext context, Object target, String name)
throws AccessException;
@Override
public TypedValue read(EvaluationContext context, Object target, String name)
throws AccessException;
@Override
public boolean canWrite(EvaluationContext context, Object target, String name)
throws AccessException;
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue)
throws AccessException;
}{ .api }
Optimized property accessor for data-binding scenarios with better performance characteristics.
public class Person {
private String name;
private int age;
private boolean active;
// Standard getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}
// Using ReflectivePropertyAccessor
StandardEvaluationContext context = new StandardEvaluationContext();
context.addPropertyAccessor(new ReflectivePropertyAccessor());
Person person = new Person();
person.setName("John");
person.setAge(30);
person.setActive(true);
ExpressionParser parser = new SpelExpressionParser();
// Reading properties
Expression exp = parser.parseExpression("name");
String name = exp.getValue(context, person, String.class); // "John"
exp = parser.parseExpression("age");
Integer age = exp.getValue(context, person, Integer.class); // 30
exp = parser.parseExpression("active");
Boolean active = exp.getValue(context, person, Boolean.class); // true
// Writing properties
exp = parser.parseExpression("name");
exp.setValue(context, person, "Jane");
// person.getName() returns "Jane"
exp = parser.parseExpression("age");
exp.setValue(context, person, 25);
// person.getAge() returns 25{ .api }
public class ReflectiveIndexAccessor implements CompilableIndexAccessor {
@Override
public Class<?>[] getSpecificTargetClasses();
@Override
public boolean canRead(EvaluationContext context, Object target, Object index)
throws AccessException;
@Override
public TypedValue read(EvaluationContext context, Object target, Object index)
throws AccessException;
@Override
public boolean canWrite(EvaluationContext context, Object target, Object index)
throws AccessException;
@Override
public void write(EvaluationContext context, Object target, Object index, Object newValue)
throws AccessException;
// CompilableIndexAccessor methods for compilation support
public boolean isCompilable();
public String generateCode(String indexedObjectName, String index, CodeFlow cf);
}{ .api }
// Array access
int[] numbers = {1, 2, 3, 4, 5};
StandardEvaluationContext context = new StandardEvaluationContext();
context.addIndexAccessor(new ReflectiveIndexAccessor());
context.setVariable("numbers", numbers);
ExpressionParser parser = new SpelExpressionParser();
// Reading array elements
Expression exp = parser.parseExpression("#numbers[0]");
Integer first = exp.getValue(context, Integer.class); // 1
exp = parser.parseExpression("#numbers[2]");
Integer third = exp.getValue(context, Integer.class); // 3
// Writing array elements
exp = parser.parseExpression("#numbers[0]");
exp.setValue(context, 10);
// numbers[0] is now 10
// List access
List<String> names = new ArrayList<>(Arrays.asList("John", "Jane", "Bob"));
context.setVariable("names", names);
exp = parser.parseExpression("#names[1]");
String name = exp.getValue(context, String.class); // "Jane"
exp = parser.parseExpression("#names[1]");
exp.setValue(context, "Janet");
// names.get(1) returns "Janet"
// Map access
Map<String, Integer> ages = new HashMap<>();
ages.put("John", 30);
ages.put("Jane", 25);
context.setVariable("ages", ages);
exp = parser.parseExpression("#ages['John']");
Integer johnAge = exp.getValue(context, Integer.class); // 30
exp = parser.parseExpression("#ages['Jane']");
exp.setValue(context, 26);
// ages.get("Jane") returns 26
// String indexing (read-only)
String text = "Hello";
context.setVariable("text", text);
exp = parser.parseExpression("#text[1]");
String character = exp.getValue(context, String.class); // "e"{ .api }
public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes {
boolean isCompilable();
String generateCode(String propertyName, String target, CodeFlow cf);
}{ .api }
public interface CompilableIndexAccessor extends IndexAccessor, Opcodes {
boolean isCompilable();
String generateCode(String indexedObjectName, String index, CodeFlow cf);
}{ .api }
public class CustomObjectPropertyAccessor implements PropertyAccessor {
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class<?>[] { CustomObject.class };
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name)
throws AccessException {
return target instanceof CustomObject &&
((CustomObject) target).hasProperty(name);
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name)
throws AccessException {
if (target instanceof CustomObject) {
CustomObject obj = (CustomObject) target;
Object value = obj.getProperty(name);
return new TypedValue(value);
}
throw new AccessException("Cannot read property: " + name);
}
@Override
public boolean canWrite(EvaluationContext context, Object target, String name)
throws AccessException {
return target instanceof CustomObject &&
((CustomObject) target).isPropertyWritable(name);
}
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue)
throws AccessException {
if (target instanceof CustomObject) {
CustomObject obj = (CustomObject) target;
obj.setProperty(name, newValue);
} else {
throw new AccessException("Cannot write property: " + name);
}
}
}
// Custom object implementation
public class CustomObject {
private final Map<String, Object> properties = new HashMap<>();
private final Set<String> readOnlyProperties = new HashSet<>();
public boolean hasProperty(String name) {
return properties.containsKey(name);
}
public Object getProperty(String name) {
return properties.get(name);
}
public void setProperty(String name, Object value) {
if (!readOnlyProperties.contains(name)) {
properties.put(name, value);
}
}
public boolean isPropertyWritable(String name) {
return !readOnlyProperties.contains(name);
}
public void markReadOnly(String name) {
readOnlyProperties.add(name);
}
}
// Usage
CustomObject obj = new CustomObject();
obj.setProperty("name", "John");
obj.setProperty("status", "active");
obj.markReadOnly("status");
StandardEvaluationContext context = new StandardEvaluationContext(obj);
context.addPropertyAccessor(new CustomObjectPropertyAccessor());
ExpressionParser parser = new SpelExpressionParser();
// Reading custom properties
Expression exp = parser.parseExpression("name");
String name = exp.getValue(context, String.class); // "John"
// Writing to writable property
exp = parser.parseExpression("name");
exp.setValue(context, "Jane");
// Attempting to write to read-only property (will be ignored)
exp = parser.parseExpression("status");
exp.setValue(context, "inactive"); // No change due to read-only{ .api }
public class MapAccessor implements PropertyAccessor {
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class<?>[] { Map.class };
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name)
throws AccessException {
return target instanceof Map;
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name)
throws AccessException {
if (target instanceof Map) {
Map<?, ?> map = (Map<?, ?>) target;
Object value = map.get(name);
return new TypedValue(value);
}
throw new AccessException("Cannot read from non-Map object");
}
@Override
public boolean canWrite(EvaluationContext context, Object target, String name)
throws AccessException {
return target instanceof Map;
}
@Override
@SuppressWarnings("unchecked")
public void write(EvaluationContext context, Object target, String name, Object newValue)
throws AccessException {
if (target instanceof Map) {
Map<String, Object> map = (Map<String, Object>) target;
map.put(name, newValue);
} else {
throw new AccessException("Cannot write to non-Map object");
}
}
}
// Usage example
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("user", "john");
dataMap.put("count", 42);
StandardEvaluationContext context = new StandardEvaluationContext(dataMap);
context.addPropertyAccessor(new MapAccessor());
ExpressionParser parser = new SpelExpressionParser();
// Access map values as properties
Expression exp = parser.parseExpression("user");
String user = exp.getValue(context, String.class); // "john"
exp = parser.parseExpression("count");
Integer count = exp.getValue(context, Integer.class); // 42
// Set new values
exp = parser.parseExpression("status");
exp.setValue(context, "active");
// dataMap now contains "status" -> "active"{ .api }
public class RangeIndexAccessor implements IndexAccessor {
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class<?>[] { List.class, String.class };
}
@Override
public boolean canRead(EvaluationContext context, Object target, Object index)
throws AccessException {
return (target instanceof List || target instanceof String) &&
index instanceof Range;
}
@Override
public TypedValue read(EvaluationContext context, Object target, Object index)
throws AccessException {
if (index instanceof Range) {
Range range = (Range) index;
if (target instanceof List) {
List<?> list = (List<?>) target;
List<Object> result = new ArrayList<>();
for (int i = range.getStart(); i <= range.getEnd() && i < list.size(); i++) {
result.add(list.get(i));
}
return new TypedValue(result);
} else if (target instanceof String) {
String str = (String) target;
int start = Math.max(0, range.getStart());
int end = Math.min(str.length(), range.getEnd() + 1);
return new TypedValue(str.substring(start, end));
}
}
throw new AccessException("Unsupported range access");
}
@Override
public boolean canWrite(EvaluationContext context, Object target, Object index)
throws AccessException {
return false; // Read-only for simplicity
}
@Override
public void write(EvaluationContext context, Object target, Object index, Object newValue)
throws AccessException {
throw new AccessException("Range write not supported");
}
}
// Range class for index specification
public class Range {
private final int start;
private final int end;
public Range(int start, int end) {
this.start = start;
this.end = end;
}
public int getStart() { return start; }
public int getEnd() { return end; }
}
// Usage would require custom expression parsing or function registration
StandardEvaluationContext context = new StandardEvaluationContext();
context.addIndexAccessor(new RangeIndexAccessor());
// This would require extending the parser or using functions
// Example conceptual usage: list[range(0, 2)] -> first 3 elements{ .api }
public class AccessorManager {
public static StandardEvaluationContext createOptimizedContext() {
StandardEvaluationContext context = new StandardEvaluationContext();
// Add accessors in priority order (most specific first)
context.addPropertyAccessor(new MapAccessor()); // Maps
context.addPropertyAccessor(new DataBindingPropertyAccessor()); // Optimized
context.addPropertyAccessor(new ReflectivePropertyAccessor()); // General
context.addIndexAccessor(new ReflectiveIndexAccessor()); // General indexing
return context;
}
public static void configureForSecurity(StandardEvaluationContext context) {
// Clear default accessors and add only safe ones
context.setPropertyAccessors(Arrays.asList(
new SafePropertyAccessor(),
new WhitelistPropertyAccessor()
));
context.setIndexAccessors(Arrays.asList(
new SafeIndexAccessor()
));
}
}
public class SafePropertyAccessor implements PropertyAccessor {
private final Set<Class<?>> allowedTypes = Set.of(
String.class, Number.class, Boolean.class, Date.class
);
@Override
public Class<?>[] getSpecificTargetClasses() {
return allowedTypes.toArray(new Class<?>[0]);
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) {
return allowedTypes.contains(target.getClass()) &&
isAllowedProperty(name);
}
private boolean isAllowedProperty(String name) {
// Whitelist approach
return name.matches("^[a-zA-Z][a-zA-Z0-9]*$") && // Simple names only
name.length() < 50; // Reasonable length
}
// ... implement other methods with safety checks
}{ .api }
public class CachingPropertyAccessor implements PropertyAccessor {
private final PropertyAccessor delegate;
private final Map<String, TypedValue> readCache = new ConcurrentHashMap<>();
private final long cacheTimeout;
private final Map<String, Long> cacheTimestamps = new ConcurrentHashMap<>();
public CachingPropertyAccessor(PropertyAccessor delegate, long cacheTimeoutMs) {
this.delegate = delegate;
this.cacheTimeout = cacheTimeoutMs;
}
@Override
public Class<?>[] getSpecificTargetClasses() {
return delegate.getSpecificTargetClasses();
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) {
return delegate.canRead(context, target, name);
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name)
throws AccessException {
String cacheKey = target.getClass().getName() + ":" + name;
Long timestamp = cacheTimestamps.get(cacheKey);
if (timestamp != null &&
System.currentTimeMillis() - timestamp < cacheTimeout) {
TypedValue cached = readCache.get(cacheKey);
if (cached != null) {
return cached;
}
}
TypedValue result = delegate.read(context, target, name);
readCache.put(cacheKey, result);
cacheTimestamps.put(cacheKey, System.currentTimeMillis());
return result;
}
@Override
public boolean canWrite(EvaluationContext context, Object target, String name) {
return delegate.canWrite(context, target, name);
}
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue)
throws AccessException {
delegate.write(context, target, name, newValue);
// Invalidate cache on write
String cacheKey = target.getClass().getName() + ":" + name;
readCache.remove(cacheKey);
cacheTimestamps.remove(cacheKey);
}
}{ .api }
// Highly optimized accessor for specific data structures
public class OptimizedDataObjectAccessor implements CompilablePropertyAccessor {
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class<?>[] { OptimizedDataObject.class };
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) {
return target instanceof OptimizedDataObject;
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name)
throws AccessException {
OptimizedDataObject obj = (OptimizedDataObject) target;
// Use optimized access methods
return switch (name) {
case "id" -> new TypedValue(obj.getId());
case "name" -> new TypedValue(obj.getName());
case "status" -> new TypedValue(obj.getStatus());
default -> throw new AccessException("Unknown property: " + name);
};
}
@Override
public boolean canWrite(EvaluationContext context, Object target, String name) {
return target instanceof OptimizedDataObject &&
!"id".equals(name); // ID is read-only
}
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue)
throws AccessException {
OptimizedDataObject obj = (OptimizedDataObject) target;
switch (name) {
case "name" -> obj.setName((String) newValue);
case "status" -> obj.setStatus((String) newValue);
default -> throw new AccessException("Cannot write property: " + name);
}
}
// Compilation support for even better performance
@Override
public boolean isCompilable() {
return true;
}
@Override
public String generateCode(String propertyName, String target, CodeFlow cf) {
// Generate bytecode for direct property access
return switch (propertyName) {
case "id" -> target + ".getId()";
case "name" -> target + ".getName()";
case "status" -> target + ".getStatus()";
default -> null;
};
}
}{ .api }
// 1. Use whitelisting for property names
public class SecurePropertyAccessor implements PropertyAccessor {
private final Set<String> allowedProperties;
private final PropertyAccessor delegate;
public SecurePropertyAccessor(PropertyAccessor delegate, String... allowedProperties) {
this.delegate = delegate;
this.allowedProperties = Set.of(allowedProperties);
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) {
return allowedProperties.contains(name) &&
delegate.canRead(context, target, name);
}
// ... implement other methods with security checks
}
// 2. Validate input types and ranges
public class ValidatingIndexAccessor implements IndexAccessor {
private final IndexAccessor delegate;
@Override
public TypedValue read(EvaluationContext context, Object target, Object index)
throws AccessException {
validateIndex(index);
return delegate.read(context, target, index);
}
private void validateIndex(Object index) throws AccessException {
if (index instanceof Integer) {
int idx = (Integer) index;
if (idx < 0 || idx > 10000) { // Reasonable bounds
throw new AccessException("Index out of safe range: " + idx);
}
}
}
}{ .api }
getSpecificTargetClasses() to avoid unnecessary checksCompilablePropertyAccessor for hot pathsTypedValue instances when possiblepublic class RobustPropertyAccessor implements PropertyAccessor {
private final PropertyAccessor delegate;
private final boolean failSilently;
public RobustPropertyAccessor(PropertyAccessor delegate, boolean failSilently) {
this.delegate = delegate;
this.failSilently = failSilently;
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name)
throws AccessException {
try {
return delegate.read(context, target, name);
} catch (Exception e) {
if (failSilently) {
return TypedValue.NULL; // Return null instead of throwing
}
throw new AccessException("Failed to read property '" + name + "'", e);
}
}
// ... implement other methods with similar error handling
}{ .api }
Install with Tessl CLI
npx tessl i tessl/maven-org-springframework--spring-expression