Spring Expression Language (SpEL) provides a powerful expression language for querying and manipulating object graphs at runtime.
—
This document covers SpEL's advanced features including expression compilation, custom functions, variables, bean references, collection operations, and integration patterns.
SpEL can compile frequently-used expressions to Java bytecode for improved performance. Compilation is controlled through SpelParserConfiguration.
// Enable immediate compilation
SpelParserConfiguration config = new SpelParserConfiguration(
SpelCompilerMode.IMMEDIATE,
Thread.currentThread().getContextClassLoader()
);
SpelExpressionParser parser = new SpelExpressionParser(config);
// Parse and compile
SpelExpression expression = parser.parseRaw("name.toUpperCase() + ' (' + age + ')'");
// First evaluation compiles the expression
String result1 = expression.getValue(person, String.class);
// Subsequent evaluations use compiled bytecode (much faster)
String result2 = expression.getValue(person, String.class);
// Check compilation status
boolean isCompiled = expression.compileExpression();
System.out.println("Expression compiled: " + isCompiled);{ .api }
// OFF: No compilation (default)
SpelParserConfiguration offConfig = new SpelParserConfiguration(
SpelCompilerMode.OFF, null
);
// IMMEDIATE: Compile after first evaluation
SpelParserConfiguration immediateConfig = new SpelParserConfiguration(
SpelCompilerMode.IMMEDIATE,
Thread.currentThread().getContextClassLoader()
);
// MIXED: Adaptive compilation based on usage patterns
SpelParserConfiguration mixedConfig = new SpelParserConfiguration(
SpelCompilerMode.MIXED,
Thread.currentThread().getContextClassLoader()
);{ .api }
SpelExpressionParser parser = new SpelExpressionParser();
SpelExpression expression = parser.parseRaw("items.![price * quantity].sum()");
// Force compilation
boolean compilationSuccessful = expression.compileExpression();
if (compilationSuccessful) {
System.out.println("Expression successfully compiled");
} else {
System.out.println("Expression could not be compiled - will use interpretation");
}
// Check if expression is currently compiled
if (expression.compileExpression()) {
System.out.println("Using compiled version");
}
// Revert to interpreted mode (useful for debugging)
expression.revertToInterpreted();
System.out.println("Reverted to interpreted mode");{ .api }
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 double distance(double x1, double y1, double x2, double y2) {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}
public static String format(String template, Object... args) {
return String.format(template, args);
}
}
StandardEvaluationContext context = new StandardEvaluationContext();
// Register static methods as functions
context.registerFunction("sqrt",
MathFunctions.class.getDeclaredMethod("sqrt", double.class));
context.registerFunction("factorial",
MathFunctions.class.getDeclaredMethod("factorial", int.class));
context.registerFunction("distance",
MathFunctions.class.getDeclaredMethod("distance", double.class, double.class, double.class, double.class));
context.registerFunction("format",
MathFunctions.class.getDeclaredMethod("format", String.class, Object[].class));
ExpressionParser parser = new SpelExpressionParser();
// Use registered functions
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("#distance(0, 0, 3, 4)");
Double distance = exp.getValue(context, Double.class); // 5.0
exp = parser.parseExpression("#format('Hello %s, you are %d years old', 'John', 30)");
String formatted = exp.getValue(context, String.class); // "Hello John, you are 30 years old"{ .api }
StandardEvaluationContext context = new StandardEvaluationContext();
// Set individual variables
context.setVariable("pi", Math.PI);
context.setVariable("greeting", "Hello");
context.setVariable("maxRetries", 3);
// Set multiple variables at once
Map<String, Object> variables = new HashMap<>();
variables.put("appName", "MyApplication");
variables.put("version", "1.0.0");
variables.put("debug", true);
context.setVariables(variables);
ExpressionParser parser = new SpelExpressionParser();
// Use variables in expressions
Expression exp = parser.parseExpression("2 * #pi");
Double circumference = exp.getValue(context, Double.class);
exp = parser.parseExpression("#greeting + ' World'");
String message = exp.getValue(context, String.class); // "Hello World"
exp = parser.parseExpression("#debug ? 'Debug mode' : 'Production mode'");
String mode = exp.getValue(context, String.class); // "Debug mode"
// Complex variable usage
exp = parser.parseExpression("#appName + ' v' + #version + (#debug ? ' (DEBUG)' : '')");
String fullName = exp.getValue(context, String.class); // "MyApplication v1.0.0 (DEBUG)"{ .api }
public class DynamicVariableProvider {
private final Map<String, Supplier<Object>> dynamicVariables = new HashMap<>();
public void registerDynamicVariable(String name, Supplier<Object> provider) {
dynamicVariables.put(name, provider);
}
public StandardEvaluationContext createContext() {
StandardEvaluationContext context = new StandardEvaluationContext();
// Set dynamic variables
dynamicVariables.forEach((name, provider) -> {
context.setVariable(name, provider.get());
});
return context;
}
}
DynamicVariableProvider provider = new DynamicVariableProvider();
provider.registerDynamicVariable("currentTime", System::currentTimeMillis);
provider.registerDynamicVariable("randomId", () -> UUID.randomUUID().toString());
provider.registerDynamicVariable("freeMemory", () -> Runtime.getRuntime().freeMemory());
// Each time we create a context, variables have fresh values
EvaluationContext context1 = provider.createContext();
EvaluationContext context2 = provider.createContext(); // Different random values
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'ID: ' + #randomId + ', Time: ' + #currentTime");
String result1 = exp.getValue(context1, String.class);
String result2 = exp.getValue(context2, String.class);
// Results will have different random IDs and timestamps{ .api }
// In a Spring application context
@Configuration
public class SpelConfiguration {
@Bean
public StandardEvaluationContext evaluationContext(ApplicationContext applicationContext) {
StandardEvaluationContext context = new StandardEvaluationContext();
// Enable bean references (@beanName syntax)
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
return context;
}
}
// Service beans
@Service
public class UserService {
public User findById(Long id) {
// Implementation...
return new User(id, "John Doe");
}
public List<User> findAll() {
// Implementation...
return Arrays.asList(
new User(1L, "John Doe"),
new User(2L, "Jane Smith")
);
}
}
@Service
public class EmailService {
public void sendEmail(String to, String subject, String body) {
System.out.printf("Sending email to %s: %s%n", to, subject);
}
}{ .api }
// Assuming Spring context with beans
ExpressionParser parser = new SpelExpressionParser();
// Reference beans using @ syntax
Expression exp = parser.parseExpression("@userService.findById(1)");
User user = exp.getValue(context, User.class);
exp = parser.parseExpression("@userService.findAll().size()");
Integer userCount = exp.getValue(context, Integer.class);
// Complex bean method chaining
exp = parser.parseExpression("@userService.findById(1).getName().toUpperCase()");
String upperName = exp.getValue(context, String.class);
// Bean references in conditional expressions
exp = parser.parseExpression("@userService.findById(1) != null ? 'User found' : 'User not found'");
String message = exp.getValue(context, String.class);
// Using beans with variables
context.setVariable("userId", 42L);
exp = parser.parseExpression("@userService.findById(#userId)");
User userById = exp.getValue(context, User.class);{ .api }
// Access factory beans using &beanName syntax
Expression exp = parser.parseExpression("&myFactoryBean");
FactoryBean<?> factory = exp.getValue(context, FactoryBean.class);
// Get the actual object created by the factory
exp = parser.parseExpression("@myFactoryBean");
Object factoryProduct = exp.getValue(context);{ .api }
public class Product {
private String name;
private double price;
private String category;
private boolean inStock;
// constructors, getters, setters...
}
List<Product> products = Arrays.asList(
new Product("Laptop", 999.99, "Electronics", true),
new Product("Mouse", 29.99, "Electronics", false),
new Product("Book", 19.99, "Books", true),
new Product("Keyboard", 79.99, "Electronics", true)
);
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("products", products);
ExpressionParser parser = new SpelExpressionParser();
// Selection: filter collection using .?[condition]
Expression exp = parser.parseExpression("#products.?[inStock == true]");
List<Product> inStockProducts = (List<Product>) exp.getValue(context);
// Filter by price range
exp = parser.parseExpression("#products.?[price >= 50 and price <= 100]");
List<Product> midRangeProducts = (List<Product>) exp.getValue(context);
// Filter by category
exp = parser.parseExpression("#products.?[category == 'Electronics']");
List<Product> electronics = (List<Product>) exp.getValue(context);
// Complex filtering
exp = parser.parseExpression("#products.?[category == 'Electronics' and inStock and price > 50]");
List<Product> availableElectronics = (List<Product>) exp.getValue(context);
// First match: .^[condition]
exp = parser.parseExpression("#products.^[price > 100]");
Product firstExpensive = (Product) exp.getValue(context);
// Last match: .$[condition]
exp = parser.parseExpression("#products.$[inStock == true]");
Product lastInStock = (Product) exp.getValue(context);{ .api }
// Projection: transform collection using .![expression]
Expression exp = parser.parseExpression("#products.![name]");
List<String> productNames = (List<String>) exp.getValue(context);
// ["Laptop", "Mouse", "Book", "Keyboard"]
exp = parser.parseExpression("#products.![name + ' - $' + price]");
List<String> nameAndPrice = (List<String>) exp.getValue(context);
// ["Laptop - $999.99", "Mouse - $29.99", ...]
// Complex projections
exp = parser.parseExpression("#products.![name.toUpperCase() + (inStock ? ' (Available)' : ' (Out of Stock)')]");
List<String> statusList = (List<String>) exp.getValue(context);
// Nested object projection
public class Order {
private List<Product> items;
// constructor, getters, setters...
}
List<Order> orders = Arrays.asList(
new Order(Arrays.asList(products.get(0), products.get(1))),
new Order(Arrays.asList(products.get(2)))
);
context.setVariable("orders", orders);
// Project nested collections
exp = parser.parseExpression("#orders.![items.![name]]");
List<List<String>> nestedNames = (List<List<String>>) exp.getValue(context);
// Flatten nested projections
exp = parser.parseExpression("#orders.![items].flatten()");
List<Product> allOrderItems = (List<Product>) exp.getValue(context);{ .api }
Map<String, Integer> inventory = new HashMap<>();
inventory.put("laptop", 10);
inventory.put("mouse", 50);
inventory.put("keyboard", 25);
context.setVariable("inventory", inventory);
// Map key selection
Expression exp = parser.parseExpression("#inventory.?[value > 20]");
Map<String, Integer> highInventory = (Map<String, Integer>) exp.getValue(context);
// {mouse=50, keyboard=25}
// Map projection (values)
exp = parser.parseExpression("#inventory.![value]");
List<Integer> inventoryCounts = (List<Integer>) exp.getValue(context);
// [10, 50, 25]
// Map projection (keys)
exp = parser.parseExpression("#inventory.![key]");
List<String> inventoryItems = (List<String>) exp.getValue(context);
// ["laptop", "mouse", "keyboard"]
// Complex map operations
exp = parser.parseExpression("#inventory.?[value < 30].![key + ': ' + value]");
List<String> lowInventoryReport = (List<String>) exp.getValue(context);
// ["laptop: 10", "keyboard: 25"]{ .api }
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
context.setVariable("numbers", numbers);
// Custom aggregation functions
context.registerFunction("sum",
Arrays.class.getDeclaredMethod("stream", Object[].class));
// Sum using projection and custom function
Expression exp = parser.parseExpression("#products.![price].stream().mapToDouble(p -> p).sum()");
// Note: This requires Java 8 streams support in the context
// Alternative: Calculate sum manually
exp = parser.parseExpression("#products.![price]");
List<Double> prices = (List<Double>) exp.getValue(context);
double totalPrice = prices.stream().mapToDouble(Double::doubleValue).sum();
// Count operations
exp = parser.parseExpression("#products.?[inStock == true].size()");
Integer inStockCount = exp.getValue(context, Integer.class);
// Min/Max operations (requires custom functions or preprocessing)
public class CollectionFunctions {
public static Double max(List<Double> values) {
return values.stream().mapToDouble(Double::doubleValue).max().orElse(0.0);
}
public static Double min(List<Double> values) {
return values.stream().mapToDouble(Double::doubleValue).min().orElse(0.0);
}
public static Double avg(List<Double> values) {
return values.stream().mapToDouble(Double::doubleValue).average().orElse(0.0);
}
}
context.registerFunction("max",
CollectionFunctions.class.getDeclaredMethod("max", List.class));
context.registerFunction("min",
CollectionFunctions.class.getDeclaredMethod("min", List.class));
context.registerFunction("avg",
CollectionFunctions.class.getDeclaredMethod("avg", List.class));
// Use aggregation functions
exp = parser.parseExpression("#max(#products.![price])");
Double maxPrice = exp.getValue(context, Double.class);
exp = parser.parseExpression("#avg(#products.![price])");
Double avgPrice = exp.getValue(context, Double.class);{ .api }
public class Address {
private String city;
private String country;
// constructors, getters, setters...
}
public class Person {
private String name;
private Address address; // might be null
// constructors, getters, setters...
}
Person person = new Person("John");
// address is null
ExpressionParser parser = new SpelExpressionParser();
// Safe navigation prevents NullPointerException
Expression exp = parser.parseExpression("address?.city");
String city = exp.getValue(person, String.class); // null, no exception
// Without safe navigation would throw NPE:
// exp = parser.parseExpression("address.city"); // NullPointerException
// Chained safe navigation
exp = parser.parseExpression("address?.city?.toUpperCase()");
String upperCity = exp.getValue(person, String.class); // null
// Safe navigation with method calls
exp = parser.parseExpression("address?.toString()?.length()");
Integer addressLength = exp.getValue(person, Integer.class); // null{ .api }
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
// Elvis operator provides default values for null
context.setVariable("name", null);
Expression exp = parser.parseExpression("#name ?: 'Unknown'");
String name = exp.getValue(context, String.class); // "Unknown"
context.setVariable("name", "John");
name = exp.getValue(context, String.class); // "John"
// Elvis with method calls
Person person = new Person(null); // name is null
exp = parser.parseExpression("name?.toUpperCase() ?: 'NO NAME'");
String upperName = exp.getValue(person, String.class); // "NO NAME"
// Complex elvis expressions
exp = parser.parseExpression("address?.city ?: address?.country ?: 'Unknown Location'");
String location = exp.getValue(person, String.class); // "Unknown Location"
// Elvis with collection operations
context.setVariable("items", null);
exp = parser.parseExpression("#items ?: {}"); // Empty map as default
Map<String, Object> items = (Map<String, Object>) exp.getValue(context);
context.setVariable("list", null);
exp = parser.parseExpression("#list ?: {1, 2, 3}"); // Default list
List<Integer> list = (List<Integer>) exp.getValue(context);{ .api }
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
// Basic pattern matching
context.setVariable("email", "user@example.com");
Expression exp = parser.parseExpression("#email matches '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}'");
Boolean isValidEmail = exp.getValue(context, Boolean.class); // true
// Case-insensitive matching
context.setVariable("text", "Hello World");
exp = parser.parseExpression("#text matches '(?i)hello.*'");
Boolean matches = exp.getValue(context, Boolean.class); // true
// Pattern matching in filtering
List<String> names = Arrays.asList("John", "Jane", "Bob", "Alice", "Andrew");
context.setVariable("names", names);
exp = parser.parseExpression("#names.?[#this matches 'A.*']"); // Names starting with 'A'
List<String> aNames = (List<String>) exp.getValue(context); // ["Alice", "Andrew"]
// Complex pattern matching
context.setVariable("phoneNumber", "+1-555-123-4567");
exp = parser.parseExpression("#phoneNumber matches '\\+1-\\d{3}-\\d{3}-\\d{4}'");
Boolean isValidPhone = exp.getValue(context, Boolean.class); // true
// URL validation
context.setVariable("url", "https://www.example.com/path?param=value");
exp = parser.parseExpression("#url matches 'https?://[\\w.-]+(:[0-9]+)?(/.*)?'");
Boolean isValidUrl = exp.getValue(context, Boolean.class); // true{ .api }
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
Map<String, Object> user = new HashMap<>();
user.put("firstName", "John");
user.put("lastName", "Doe");
user.put("age", 30);
user.put("city", "New York");
context.setVariables(user);
// Template with multiple expressions
String template = "Hello #{#firstName} #{#lastName}! You are #{#age} years old and live in #{#city}.";
Expression exp = parser.parseExpression(template, ParserContext.TEMPLATE_EXPRESSION);
String message = exp.getValue(context, String.class);
// "Hello John Doe! You are 30 years old and live in New York."
// Conditional templates
template = "Welcome #{#firstName}#{#age >= 18 ? ' (Adult)' : ' (Minor)'}!";
exp = parser.parseExpression(template, ParserContext.TEMPLATE_EXPRESSION);
String welcome = exp.getValue(context, String.class);
// "Welcome John (Adult)!"
// Mathematical expressions in templates
template = "Next year you will be #{#age + 1} years old.";
exp = parser.parseExpression(template, ParserContext.TEMPLATE_EXPRESSION);
String nextYear = exp.getValue(context, String.class);
// "Next year you will be 31 years old."{ .api }
// Custom template delimiters
TemplateParserContext customContext = new TemplateParserContext("${", "}");
String template = "Hello ${#name}, today is ${T(java.time.LocalDate).now()}";
Expression exp = parser.parseExpression(template, customContext);
context.setVariable("name", "World");
String result = exp.getValue(context, String.class);
// "Hello World, today is 2023-12-07"
// XML-friendly delimiters
TemplateParserContext xmlContext = new TemplateParserContext("{{", "}}");
template = "<greeting>Hello {{#name}}</greeting>";
exp = parser.parseExpression(template, xmlContext);
result = exp.getValue(context, String.class);
// "<greeting>Hello World</greeting>"{ .api }
public class ExpressionComposer {
private final ExpressionParser parser = new SpelExpressionParser();
private final Map<String, Expression> namedExpressions = new HashMap<>();
public void defineExpression(String name, String expressionString) {
Expression expr = parser.parseExpression(expressionString);
namedExpressions.put(name, expr);
}
public Expression composeExpression(String... expressionNames) {
// Create composite expression that evaluates multiple sub-expressions
StringBuilder composite = new StringBuilder();
for (int i = 0; i < expressionNames.length; i++) {
if (i > 0) {
composite.append(" + ");
}
composite.append("(").append(getExpressionString(expressionNames[i])).append(")");
}
return parser.parseExpression(composite.toString());
}
private String getExpressionString(String name) {
Expression expr = namedExpressions.get(name);
return expr != null ? expr.getExpressionString() : "''";
}
public Object evaluateComposite(EvaluationContext context, String... expressionNames) {
Expression composite = composeExpression(expressionNames);
return composite.getValue(context);
}
}
// Usage
ExpressionComposer composer = new ExpressionComposer();
composer.defineExpression("greeting", "'Hello '");
composer.defineExpression("name", "#user.name");
composer.defineExpression("suffix", "', welcome!'");
Expression greeting = composer.composeExpression("greeting", "name", "suffix");
context.setVariable("user", new User("Alice"));
String result = greeting.getValue(context, String.class);
// "Hello Alice, welcome!"{ .api }
public class OptimizedExpressionService {
private final ExpressionParser parser;
private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();
private final Map<String, AtomicLong> usageStats = new ConcurrentHashMap<>();
public OptimizedExpressionService() {
// Configure for performance
SpelParserConfiguration config = new SpelParserConfiguration(
SpelCompilerMode.IMMEDIATE,
Thread.currentThread().getContextClassLoader(),
false, // Disable auto-grow for predictability
false,
0,
10000
);
this.parser = new SpelExpressionParser(config);
}
public Expression getExpression(String expressionString) {
return expressionCache.computeIfAbsent(expressionString, expr -> {
System.out.println("Compiling new expression: " + expr);
return parser.parseExpression(expr);
});
}
public Object evaluate(String expressionString, EvaluationContext context, Object rootObject) {
Expression expr = getExpression(expressionString);
// Track usage
usageStats.computeIfAbsent(expressionString, k -> new AtomicLong()).incrementAndGet();
return expr.getValue(context, rootObject);
}
public void printUsageStats() {
System.out.println("Expression Usage Statistics:");
usageStats.entrySet().stream()
.sorted(Map.Entry.<String, AtomicLong>comparingByValue(
(a, b) -> Long.compare(b.get(), a.get())))
.limit(10)
.forEach(entry ->
System.out.printf(" %dx: %s%n", entry.getValue().get(), entry.getKey()));
}
// Precompile frequently used expressions
public void warmUp(String... expressions) {
for (String expr : expressions) {
Expression compiled = getExpression(expr);
if (compiled instanceof SpelExpression) {
((SpelExpression) compiled).compileExpression();
}
}
}
}
// Usage
OptimizedExpressionService service = new OptimizedExpressionService();
// Warm up with known frequent expressions
service.warmUp(
"user.name.toUpperCase()",
"order.items.![price * quantity].sum()",
"product.inStock and product.price > 0"
);
// Use service
Object result = service.evaluate("user.name.toUpperCase()", context, user);{ .api }
public class PerformanceEvaluationContext extends StandardEvaluationContext {
public PerformanceEvaluationContext() {
super();
configureForPerformance();
}
public PerformanceEvaluationContext(Object rootObject) {
super(rootObject);
configureForPerformance();
}
private void configureForPerformance() {
// Use optimized accessors in order of likely usage
setPropertyAccessors(Arrays.asList(
new DataBindingPropertyAccessor(), // Fastest for simple properties
new ReflectivePropertyAccessor(false) // Read-only for security & speed
));
// Minimal method resolution
setMethodResolvers(Arrays.asList(
new DataBindingMethodResolver()
));
// Optimized type handling
setTypeConverter(new StandardTypeConverter());
setTypeComparator(new StandardTypeComparator());
setOperatorOverloader(new StandardOperatorOverloader());
// Fast type locator with common imports
StandardTypeLocator typeLocator = new StandardTypeLocator();
typeLocator.registerImport("", "java.lang");
typeLocator.registerImport("", "java.util");
typeLocator.registerImport("", "java.time");
setTypeLocator(typeLocator);
}
// Bulk variable setting for reduced overhead
public void setVariablesBulk(Map<String, Object> variables) {
variables.forEach(this::setVariable);
}
}{ .api }
@Configuration
@EnableConfigurationProperties(SpelProperties.class)
public class SpelAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SpelExpressionParser spelExpressionParser(SpelProperties properties) {
SpelParserConfiguration config = new SpelParserConfiguration(
properties.getCompilerMode(),
Thread.currentThread().getContextClassLoader(),
properties.isAutoGrowNullReferences(),
properties.isAutoGrowCollections(),
properties.getMaximumAutoGrowSize(),
properties.getMaximumExpressionLength()
);
return new SpelExpressionParser(config);
}
@Bean
@ConditionalOnMissingBean
public StandardEvaluationContext standardEvaluationContext(
ApplicationContext applicationContext,
SpelProperties properties) {
StandardEvaluationContext context = new StandardEvaluationContext();
if (properties.isBeanReferencesEnabled()) {
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
}
return context;
}
}
@ConfigurationProperties(prefix = "spel")
public class SpelProperties {
private SpelCompilerMode compilerMode = SpelCompilerMode.OFF;
private boolean autoGrowNullReferences = false;
private boolean autoGrowCollections = false;
private int maximumAutoGrowSize = Integer.MAX_VALUE;
private int maximumExpressionLength = 10000;
private boolean beanReferencesEnabled = true;
// getters and setters...
}{ .api }
@Component
public class ExpressionValidator {
private final SpelExpressionParser parser = new SpelExpressionParser();
public boolean validate(Object object, String validationExpression) {
try {
Expression expr = parser.parseExpression(validationExpression);
Boolean result = expr.getValue(object, Boolean.class);
return result != null && result;
} catch (Exception e) {
return false;
}
}
// Annotation-based validation
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ExpressionConstraintValidator.class)
public @interface ValidExpression {
String value();
String message() default "Expression validation failed";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public static class ExpressionConstraintValidator
implements ConstraintValidator<ValidExpression, Object> {
private String expression;
private final ExpressionValidator validator = new ExpressionValidator();
@Override
public void initialize(ValidExpression constraint) {
this.expression = constraint.value();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
return value == null || validator.validate(value, expression);
}
}
}
// Usage
@ValidExpression("age >= 18 and age <= 120")
public class Person {
private int age;
private String name;
// getters and setters...
}{ .api }
// Secure expression evaluation service
public class SecureExpressionService {
private final SpelExpressionParser parser;
private final Set<String> allowedExpressionPatterns;
public SecureExpressionService() {
// Disable compilation for security
SpelParserConfiguration config = new SpelParserConfiguration(
SpelCompilerMode.OFF, null
);
this.parser = new SpelExpressionParser(config);
// Define allowed expression patterns
this.allowedExpressionPatterns = Set.of(
"^[a-zA-Z_][a-zA-Z0-9_.]*$", // Simple property access
"^#[a-zA-Z_][a-zA-Z0-9_]*$", // Variable access
"^[^T(]*$" // No type references
);
}
public Object safeEvaluate(String expression, EvaluationContext context, Object root) {
// Validate expression against security rules
if (!isExpressionSafe(expression)) {
throw new SecurityException("Expression not allowed: " + expression);
}
// Use restricted context
SimpleEvaluationContext secureContext = SimpleEvaluationContext
.forReadOnlyDataBinding()
.build();
Expression expr = parser.parseExpression(expression);
return expr.getValue(secureContext, root);
}
private boolean isExpressionSafe(String expression) {
return allowedExpressionPatterns.stream()
.anyMatch(pattern -> expression.matches(pattern)) &&
!containsDangerousPatterns(expression);
}
private boolean containsDangerousPatterns(String expression) {
String[] dangerousPatterns = {
"T(", "new ", "getClass", "forName", "invoke"
};
return Arrays.stream(dangerousPatterns)
.anyMatch(expression::contains);
}
}{ .api }
Install with Tessl CLI
npx tessl i tessl/maven-org-springframework--spring-expression