-
Notifications
You must be signed in to change notification settings - Fork 0
Dynamic Keys with SpEL
Locksmith supports Spring Expression Language (SpEL) for creating dynamic lock keys based on method parameters.
SpEL expressions MUST be wrapped in #{...} syntax:
// Correct - SpEL expression
@DistributedLock(key = "#{#userId}")
public void updateUser(String userId) { }
// Incorrect - treated as literal string "#userId"
@DistributedLock(key = "#userId")
public void updateUser(String userId) { }Literal keys can contain # character:
// These are all literal keys (not SpEL)
@DistributedLock(key = "order#123") // Literal
@DistributedLock(key = "task#end") // Literal
@DistributedLock(key = "#userId") // Literal (no #{} wrapper)| Key Expression | Type | Resolved Value |
|---|---|---|
"#{#userId}" |
SpEL | Value of userId parameter |
"#userId" |
Literal | String "#userId"
|
"order#123" |
Literal | String "order#123"
|
"#{'user-' + #id}" |
SpEL | Concatenated string like "user-42"
|
"#{#user.name}" |
SpEL | Value of user.name property |
Static keys lock globally:
// All users blocked while any user's order is processing
@DistributedLock(key = "process-order")
public void processOrder(String userId, String orderId) { }Dynamic keys enable fine-grained locking:
// Only the specific order is locked
@DistributedLock(key = "#{'order-' + #orderId}")
public void processOrder(String userId, String orderId) { }Use #{#parameterName} to reference method parameters:
@DistributedLock(key = "#{#userId}")
public void updateUser(String userId) {
// Lock key: lock:user123
}
@DistributedLock(key = "#{#orderId}")
public void processOrder(Long orderId) {
// Lock key: lock:12345
}Combine literals and parameters with +:
@DistributedLock(key = "#{'user-' + #userId}")
public void updateUser(String userId) {
// Lock key: lock:user-user123
}
@DistributedLock(key = "#{'order-' + #orderId + '-process'}")
public void processOrder(Long orderId) {
// Lock key: lock:order-12345-process
}Note: String literals in SpEL use single quotes:
'literal'
Access object properties with dot notation:
@DistributedLock(key = "#{#user.id}")
public void updateUser(User user) {
// Lock key: lock:123
}
@DistributedLock(key = "#{#order.customer.id}")
public void processOrder(Order order) {
// Lock key: lock:456
}
@DistributedLock(key = "#{'user-' + #user.email}")
public void sendEmail(User user) {
// Lock key: lock:user-john@example.com
}Combine multiple parameters:
@DistributedLock(key = "#{#tenantId + '-' + #userId}")
public void updateUserInTenant(String tenantId, String userId) {
// Lock key: lock:tenant1-user123
}
@DistributedLock(key = "#{'transfer-' + #fromAccount + '-to-' + #toAccount}")
public void transfer(String fromAccount, String toAccount, BigDecimal amount) {
// Lock key: lock:transfer-ACC001-to-ACC002
}Access parameters by position using #p0, #p1, etc. or #a0, #a1, etc.:
@DistributedLock(key = "#{#p0}") // First parameter
public void processUser(String userId) {
// Lock key: lock:user123
}
@DistributedLock(key = "#{#a0 + '-' + #a1}") // First and second parameters
public void process(String first, String second) {
// Lock key: lock:first-second
}Call methods on parameters:
@DistributedLock(key = "#{#email.toLowerCase()}")
public void processEmail(String email) {
// Lock key: lock:john@example.com (normalized)
}
@DistributedLock(key = "#{#list.size()}")
public void processList(List<String> list) {
// Lock key: lock:5
}
@DistributedLock(key = "#{#date.toLocalDate().toString()}")
public void processForDate(LocalDateTime date) {
// Lock key: lock:2024-01-15
}
@DistributedLock(key = "#{#user.getId()}")
public void processUser(User user) {
// Lock key: lock:123
}Use ternary operator for conditional keys:
@DistributedLock(key = "#{#premium ? 'premium-' + #userId : 'standard-' + #userId}")
public void processUser(String userId, boolean premium) {
// premium=true: lock:premium-user123
// premium=false: lock:standard-user123
}
@DistributedLock(key = "#{#amount > 1000 ? 'large' : 'small'}")
public void processPayment(double amount) {
// amount=1500: lock:large
// amount=500: lock:small
}Handle potentially null values:
// Using Elvis operator
@DistributedLock(key = "#{#user.nickname ?: #user.id}")
public void updateUser(User user) {
// Uses nickname if present, otherwise id
}
// Using safe navigation
@DistributedLock(key = "#{#order?.customer?.id ?: 'anonymous'}")
public void processOrder(Order order) {
// Returns 'anonymous' if order, customer, or id is null
}Access elements by index:
@DistributedLock(key = "#{#ids[0]}")
public void processFirst(List<String> ids) {
// Lock key: lock:first-id
}
@DistributedLock(key = "#{#args[0] + '-' + #args[1]}")
public void process(String... args) {
// Lock key: lock:arg1-arg2
}
@DistributedLock(key = "#{#users[0].name}")
public void processFirstUser(List<User> users) {
// Lock key: lock:John
}Access map values:
@DistributedLock(key = "#{#params['orderId']}")
public void process(Map<String, String> params) {
// Lock key: lock:order123
}
@DistributedLock(key = "#{#context['tenant'] + '-' + #context['user']}")
public void processInContext(Map<String, Object> context) {
// Lock key: lock:tenant1-user123
}Reference static members using T() syntax:
@DistributedLock(key = "#{T(java.util.UUID).randomUUID().toString()}")
public void uniqueLock() {
// Lock key: lock:550e8400-e29b-41d4-a716-446655440000
// Warning: This creates a new lock every time!
}
@DistributedLock(key = "#{'version-' + T(com.example.AppVersion).CURRENT}")
public void migrate() {
// Lock key: lock:version-2.0.0
}
@DistributedLock(key = "#{T(java.lang.String).valueOf(#orderId)}")
public void processOrder(Long orderId) {
// Lock key: lock:12345
}
@DistributedLock(key = "#{T(java.lang.Math).max(#a, #b)}")
public void processMax(int a, int b) {
// Lock key: lock:100 (if max is 100)
}@DistributedLock(key = "#{'user-profile-' + #userId}")
public void updateProfile(String userId, Profile profile) { }
@DistributedLock(key = "#{'user-settings-' + #user.id}")
public void updateSettings(User user, Settings settings) { }
@RateLimit(key = "#{'user-' + #userId}", permits = 100, interval = "1m")
public void processUserRequest(String userId) { }@DistributedLock(key = "#{'document-' + #documentId}")
public void editDocument(String documentId, String content) { }
@DistributedLock(key = "#{'file-' + #path.hashCode()}")
public void writeFile(Path path, byte[] data) { }@DistributedLock(key = "#{#tenantId + ':user:' + #userId}")
public void updateTenantUser(String tenantId, String userId) {
// Lock key: lock:tenant1:user:user123
}@DistributedLock(key = "#{'inventory-' + #warehouseId + '-' + #productId}")
public void updateInventory(String warehouseId, String productId, int quantity) {
// Lock key: lock:inventory-WH001-PROD123
}@DistributedLock(key = "#{'daily-report-' + T(java.time.LocalDate).now().toString()}")
public void generateDailyReport() {
// Lock key: lock:daily-report-2024-01-15
}
@DistributedLock(key = "#{'hourly-' + #instant.truncatedTo(T(java.time.temporal.ChronoUnit).HOURS)}")
public void hourlyTask(Instant instant) { }Since SpEL requires #{...} wrapper, you can use # in literal keys without any issues:
// All these are literal keys (not SpEL)
@DistributedLock(key = "order#123")
public void processOrder() {
// Lock key: lock:order#123
}
@DistributedLock(key = "task#end")
public void endTask() {
// Lock key: lock:task#end
}
@DistributedLock(key = "item-#1")
public void processItem() {
// Lock key: lock:item-#1
}
@DistributedLock(key = "prefix#middle#suffix")
public void process() {
// Lock key: lock:prefix#middle#suffix
}Locksmith validates SpEL expressions at runtime:
@DistributedLock(key = "#{#user.id}")
public void process(User user) { }
// If user.id is null:
// IllegalArgumentException: SpEL expression '#user.id' evaluated to null@DistributedLock(key = "#{#name}")
public void process(String name) { }
// If name is "" or " ":
// IllegalArgumentException: SpEL expression '#name' evaluated to blank string@DistributedLock(key = "#{#nonexistent}")
public void process(String value) { }
// If parameter doesn't exist:
// SpelEvaluationException: Property or field 'nonexistent' cannot be found// Correct - SpEL will be evaluated
@DistributedLock(key = "#{#userId}")
// Incorrect - treated as literal "#userId"
@DistributedLock(key = "#userId")
// Correct - literal key (no evaluation needed)
@DistributedLock(key = "static-key")// Good
@DistributedLock(key = "#{'ord-' + #orderId}")
// Verbose but acceptable
@DistributedLock(key = "#{'order-processing-' + #orderId}")
// Too long - wastes Redis memory
@DistributedLock(key = "#{'order-processing-workflow-step-1-' + #orderId}")// Normalize email to prevent duplicates
@DistributedLock(key = "#{'email-' + #email.toLowerCase().trim()}")
public void processEmail(String email) { }@DistributedLock(key = "#{'read:' + #docId}")
public Document readDocument(String docId) { }
@DistributedLock(key = "#{'write:' + #docId}")
public void writeDocument(String docId, Document doc) { }// Bad - different key each time
@DistributedLock(key = "#{T(java.util.UUID).randomUUID()}")
// Bad - time-dependent
@DistributedLock(key = "#{T(System).currentTimeMillis()}")
// Good - deterministic based on input
@DistributedLock(key = "#{#userId}")If you're upgrading from an earlier version that didn't require #{...} wrapper:
Before (old syntax):
@DistributedLock(key = "#userId")
@DistributedLock(key = "'user-' + #id")
@DistributedLock(key = "#user.name")After (new syntax):
@DistributedLock(key = "#{#userId}")
@DistributedLock(key = "#{'user-' + #id}")
@DistributedLock(key = "#{#user.name}")Benefits of new syntax:
- Literal keys can contain
#character (e.g.,order#123) - Clear distinction between SpEL and literal keys
- Consistent with Spring's property placeholder syntax
- No ambiguity or special escaping needed
For comprehensive examples demonstrating all SpEL features documented on this page, see the test suite:
This test file contains working examples of:
- Basic parameter references (
#{#userId},#{#orderId}) - String concatenation (
#{'user-' + #id}) - Object property access (
#{#user.name},#{#order.customer.id}) - Multiple parameter combinations (
#{#tenantId + '-' + #userId}) - Parameter access by position (
#{#p0},#{#a0}) - Method invocations (
#{#email.toLowerCase()},#{#list.size()}) - Conditional expressions (
#{#premium ? 'premium-' + #userId : 'standard-' + #userId}) - Null safety operators (
#{#user.nickname ?: #user.id},#{#order?.customer?.id}) - Collection and array access (
#{#users[0].name}) - Map access (
#{#params['orderId']}) - Static method calls (
#{T(java.lang.String).valueOf(#id)}) - Common patterns (per-user, per-resource, per-tenant locking)
- Normalized keys (
#{#email.toLowerCase().trim()})
Each example in the test suite validates that the SpEL expression correctly resolves to the expected lock key.
- Lock Types - Read/Write locks for concurrent access patterns
- Lock Acquisition Modes - Wait for lock availability
- Annotation Reference - Complete reference for all attributes
