0
# Property and Index Access
1
2
This document covers SpEL's property and index access capabilities, including accessor interfaces, standard implementations, and custom accessor development.
3
4
## Core Accessor Interfaces
5
6
### TargetedAccessor Interface
7
8
```java
9
public interface TargetedAccessor {
10
Class<?>[] getSpecificTargetClasses();
11
}
12
```
13
{ .api }
14
15
The base interface for accessors that target specific classes for optimization.
16
17
### PropertyAccessor Interface
18
19
```java
20
public interface PropertyAccessor extends TargetedAccessor {
21
boolean canRead(EvaluationContext context, Object target, String name) throws AccessException;
22
TypedValue read(EvaluationContext context, Object target, String name) throws AccessException;
23
boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException;
24
void write(EvaluationContext context, Object target, String name, Object newValue)
25
throws AccessException;
26
}
27
```
28
{ .api }
29
30
### IndexAccessor Interface
31
32
```java
33
public interface IndexAccessor extends TargetedAccessor {
34
boolean canRead(EvaluationContext context, Object target, Object index) throws AccessException;
35
TypedValue read(EvaluationContext context, Object target, Object index) throws AccessException;
36
boolean canWrite(EvaluationContext context, Object target, Object index) throws AccessException;
37
void write(EvaluationContext context, Object target, Object index, Object newValue)
38
throws AccessException;
39
}
40
```
41
{ .api }
42
43
## Standard Property Accessors
44
45
### ReflectivePropertyAccessor Class
46
47
```java
48
public class ReflectivePropertyAccessor implements PropertyAccessor {
49
public ReflectivePropertyAccessor();
50
public ReflectivePropertyAccessor(boolean allowWrite);
51
52
@Override
53
public Class<?>[] getSpecificTargetClasses();
54
55
@Override
56
public boolean canRead(EvaluationContext context, Object target, String name)
57
throws AccessException;
58
@Override
59
public TypedValue read(EvaluationContext context, Object target, String name)
60
throws AccessException;
61
@Override
62
public boolean canWrite(EvaluationContext context, Object target, String name)
63
throws AccessException;
64
@Override
65
public void write(EvaluationContext context, Object target, String name, Object newValue)
66
throws AccessException;
67
}
68
```
69
{ .api }
70
71
The default property accessor that uses Java reflection to access object properties via getter/setter methods and fields.
72
73
### DataBindingPropertyAccessor Class
74
75
```java
76
public class DataBindingPropertyAccessor implements PropertyAccessor {
77
@Override
78
public Class<?>[] getSpecificTargetClasses();
79
80
@Override
81
public boolean canRead(EvaluationContext context, Object target, String name)
82
throws AccessException;
83
@Override
84
public TypedValue read(EvaluationContext context, Object target, String name)
85
throws AccessException;
86
@Override
87
public boolean canWrite(EvaluationContext context, Object target, String name)
88
throws AccessException;
89
@Override
90
public void write(EvaluationContext context, Object target, String name, Object newValue)
91
throws AccessException;
92
}
93
```
94
{ .api }
95
96
Optimized property accessor for data-binding scenarios with better performance characteristics.
97
98
### Property Access Examples
99
100
```java
101
public class Person {
102
private String name;
103
private int age;
104
private boolean active;
105
106
// Standard getters and setters
107
public String getName() { return name; }
108
public void setName(String name) { this.name = name; }
109
public int getAge() { return age; }
110
public void setAge(int age) { this.age = age; }
111
public boolean isActive() { return active; }
112
public void setActive(boolean active) { this.active = active; }
113
}
114
115
// Using ReflectivePropertyAccessor
116
StandardEvaluationContext context = new StandardEvaluationContext();
117
context.addPropertyAccessor(new ReflectivePropertyAccessor());
118
119
Person person = new Person();
120
person.setName("John");
121
person.setAge(30);
122
person.setActive(true);
123
124
ExpressionParser parser = new SpelExpressionParser();
125
126
// Reading properties
127
Expression exp = parser.parseExpression("name");
128
String name = exp.getValue(context, person, String.class); // "John"
129
130
exp = parser.parseExpression("age");
131
Integer age = exp.getValue(context, person, Integer.class); // 30
132
133
exp = parser.parseExpression("active");
134
Boolean active = exp.getValue(context, person, Boolean.class); // true
135
136
// Writing properties
137
exp = parser.parseExpression("name");
138
exp.setValue(context, person, "Jane");
139
// person.getName() returns "Jane"
140
141
exp = parser.parseExpression("age");
142
exp.setValue(context, person, 25);
143
// person.getAge() returns 25
144
```
145
{ .api }
146
147
## Standard Index Accessors
148
149
### ReflectiveIndexAccessor Class
150
151
```java
152
public class ReflectiveIndexAccessor implements CompilableIndexAccessor {
153
@Override
154
public Class<?>[] getSpecificTargetClasses();
155
156
@Override
157
public boolean canRead(EvaluationContext context, Object target, Object index)
158
throws AccessException;
159
@Override
160
public TypedValue read(EvaluationContext context, Object target, Object index)
161
throws AccessException;
162
@Override
163
public boolean canWrite(EvaluationContext context, Object target, Object index)
164
throws AccessException;
165
@Override
166
public void write(EvaluationContext context, Object target, Object index, Object newValue)
167
throws AccessException;
168
169
// CompilableIndexAccessor methods for compilation support
170
public boolean isCompilable();
171
public String generateCode(String indexedObjectName, String index, CodeFlow cf);
172
}
173
```
174
{ .api }
175
176
### Index Access Examples
177
178
```java
179
// Array access
180
int[] numbers = {1, 2, 3, 4, 5};
181
StandardEvaluationContext context = new StandardEvaluationContext();
182
context.addIndexAccessor(new ReflectiveIndexAccessor());
183
context.setVariable("numbers", numbers);
184
185
ExpressionParser parser = new SpelExpressionParser();
186
187
// Reading array elements
188
Expression exp = parser.parseExpression("#numbers[0]");
189
Integer first = exp.getValue(context, Integer.class); // 1
190
191
exp = parser.parseExpression("#numbers[2]");
192
Integer third = exp.getValue(context, Integer.class); // 3
193
194
// Writing array elements
195
exp = parser.parseExpression("#numbers[0]");
196
exp.setValue(context, 10);
197
// numbers[0] is now 10
198
199
// List access
200
List<String> names = new ArrayList<>(Arrays.asList("John", "Jane", "Bob"));
201
context.setVariable("names", names);
202
203
exp = parser.parseExpression("#names[1]");
204
String name = exp.getValue(context, String.class); // "Jane"
205
206
exp = parser.parseExpression("#names[1]");
207
exp.setValue(context, "Janet");
208
// names.get(1) returns "Janet"
209
210
// Map access
211
Map<String, Integer> ages = new HashMap<>();
212
ages.put("John", 30);
213
ages.put("Jane", 25);
214
context.setVariable("ages", ages);
215
216
exp = parser.parseExpression("#ages['John']");
217
Integer johnAge = exp.getValue(context, Integer.class); // 30
218
219
exp = parser.parseExpression("#ages['Jane']");
220
exp.setValue(context, 26);
221
// ages.get("Jane") returns 26
222
223
// String indexing (read-only)
224
String text = "Hello";
225
context.setVariable("text", text);
226
227
exp = parser.parseExpression("#text[1]");
228
String character = exp.getValue(context, String.class); // "e"
229
```
230
{ .api }
231
232
## Compilation Support
233
234
### CompilablePropertyAccessor Interface
235
236
```java
237
public interface CompilablePropertyAccessor extends PropertyAccessor, Opcodes {
238
boolean isCompilable();
239
String generateCode(String propertyName, String target, CodeFlow cf);
240
}
241
```
242
{ .api }
243
244
### CompilableIndexAccessor Interface
245
246
```java
247
public interface CompilableIndexAccessor extends IndexAccessor, Opcodes {
248
boolean isCompilable();
249
String generateCode(String indexedObjectName, String index, CodeFlow cf);
250
}
251
```
252
{ .api }
253
254
## Custom Property Accessors
255
256
### Basic Custom Property Accessor
257
258
```java
259
public class CustomObjectPropertyAccessor implements PropertyAccessor {
260
261
@Override
262
public Class<?>[] getSpecificTargetClasses() {
263
return new Class<?>[] { CustomObject.class };
264
}
265
266
@Override
267
public boolean canRead(EvaluationContext context, Object target, String name)
268
throws AccessException {
269
return target instanceof CustomObject &&
270
((CustomObject) target).hasProperty(name);
271
}
272
273
@Override
274
public TypedValue read(EvaluationContext context, Object target, String name)
275
throws AccessException {
276
if (target instanceof CustomObject) {
277
CustomObject obj = (CustomObject) target;
278
Object value = obj.getProperty(name);
279
return new TypedValue(value);
280
}
281
throw new AccessException("Cannot read property: " + name);
282
}
283
284
@Override
285
public boolean canWrite(EvaluationContext context, Object target, String name)
286
throws AccessException {
287
return target instanceof CustomObject &&
288
((CustomObject) target).isPropertyWritable(name);
289
}
290
291
@Override
292
public void write(EvaluationContext context, Object target, String name, Object newValue)
293
throws AccessException {
294
if (target instanceof CustomObject) {
295
CustomObject obj = (CustomObject) target;
296
obj.setProperty(name, newValue);
297
} else {
298
throw new AccessException("Cannot write property: " + name);
299
}
300
}
301
}
302
303
// Custom object implementation
304
public class CustomObject {
305
private final Map<String, Object> properties = new HashMap<>();
306
private final Set<String> readOnlyProperties = new HashSet<>();
307
308
public boolean hasProperty(String name) {
309
return properties.containsKey(name);
310
}
311
312
public Object getProperty(String name) {
313
return properties.get(name);
314
}
315
316
public void setProperty(String name, Object value) {
317
if (!readOnlyProperties.contains(name)) {
318
properties.put(name, value);
319
}
320
}
321
322
public boolean isPropertyWritable(String name) {
323
return !readOnlyProperties.contains(name);
324
}
325
326
public void markReadOnly(String name) {
327
readOnlyProperties.add(name);
328
}
329
}
330
331
// Usage
332
CustomObject obj = new CustomObject();
333
obj.setProperty("name", "John");
334
obj.setProperty("status", "active");
335
obj.markReadOnly("status");
336
337
StandardEvaluationContext context = new StandardEvaluationContext(obj);
338
context.addPropertyAccessor(new CustomObjectPropertyAccessor());
339
340
ExpressionParser parser = new SpelExpressionParser();
341
342
// Reading custom properties
343
Expression exp = parser.parseExpression("name");
344
String name = exp.getValue(context, String.class); // "John"
345
346
// Writing to writable property
347
exp = parser.parseExpression("name");
348
exp.setValue(context, "Jane");
349
350
// Attempting to write to read-only property (will be ignored)
351
exp = parser.parseExpression("status");
352
exp.setValue(context, "inactive"); // No change due to read-only
353
```
354
{ .api }
355
356
### Map-based Property Accessor
357
358
```java
359
public class MapAccessor implements PropertyAccessor {
360
361
@Override
362
public Class<?>[] getSpecificTargetClasses() {
363
return new Class<?>[] { Map.class };
364
}
365
366
@Override
367
public boolean canRead(EvaluationContext context, Object target, String name)
368
throws AccessException {
369
return target instanceof Map;
370
}
371
372
@Override
373
public TypedValue read(EvaluationContext context, Object target, String name)
374
throws AccessException {
375
if (target instanceof Map) {
376
Map<?, ?> map = (Map<?, ?>) target;
377
Object value = map.get(name);
378
return new TypedValue(value);
379
}
380
throw new AccessException("Cannot read from non-Map object");
381
}
382
383
@Override
384
public boolean canWrite(EvaluationContext context, Object target, String name)
385
throws AccessException {
386
return target instanceof Map;
387
}
388
389
@Override
390
@SuppressWarnings("unchecked")
391
public void write(EvaluationContext context, Object target, String name, Object newValue)
392
throws AccessException {
393
if (target instanceof Map) {
394
Map<String, Object> map = (Map<String, Object>) target;
395
map.put(name, newValue);
396
} else {
397
throw new AccessException("Cannot write to non-Map object");
398
}
399
}
400
}
401
402
// Usage example
403
Map<String, Object> dataMap = new HashMap<>();
404
dataMap.put("user", "john");
405
dataMap.put("count", 42);
406
407
StandardEvaluationContext context = new StandardEvaluationContext(dataMap);
408
context.addPropertyAccessor(new MapAccessor());
409
410
ExpressionParser parser = new SpelExpressionParser();
411
412
// Access map values as properties
413
Expression exp = parser.parseExpression("user");
414
String user = exp.getValue(context, String.class); // "john"
415
416
exp = parser.parseExpression("count");
417
Integer count = exp.getValue(context, Integer.class); // 42
418
419
// Set new values
420
exp = parser.parseExpression("status");
421
exp.setValue(context, "active");
422
// dataMap now contains "status" -> "active"
423
```
424
{ .api }
425
426
## Custom Index Accessors
427
428
### Range-based Index Accessor
429
430
```java
431
public class RangeIndexAccessor implements IndexAccessor {
432
433
@Override
434
public Class<?>[] getSpecificTargetClasses() {
435
return new Class<?>[] { List.class, String.class };
436
}
437
438
@Override
439
public boolean canRead(EvaluationContext context, Object target, Object index)
440
throws AccessException {
441
return (target instanceof List || target instanceof String) &&
442
index instanceof Range;
443
}
444
445
@Override
446
public TypedValue read(EvaluationContext context, Object target, Object index)
447
throws AccessException {
448
if (index instanceof Range) {
449
Range range = (Range) index;
450
451
if (target instanceof List) {
452
List<?> list = (List<?>) target;
453
List<Object> result = new ArrayList<>();
454
for (int i = range.getStart(); i <= range.getEnd() && i < list.size(); i++) {
455
result.add(list.get(i));
456
}
457
return new TypedValue(result);
458
} else if (target instanceof String) {
459
String str = (String) target;
460
int start = Math.max(0, range.getStart());
461
int end = Math.min(str.length(), range.getEnd() + 1);
462
return new TypedValue(str.substring(start, end));
463
}
464
}
465
throw new AccessException("Unsupported range access");
466
}
467
468
@Override
469
public boolean canWrite(EvaluationContext context, Object target, Object index)
470
throws AccessException {
471
return false; // Read-only for simplicity
472
}
473
474
@Override
475
public void write(EvaluationContext context, Object target, Object index, Object newValue)
476
throws AccessException {
477
throw new AccessException("Range write not supported");
478
}
479
}
480
481
// Range class for index specification
482
public class Range {
483
private final int start;
484
private final int end;
485
486
public Range(int start, int end) {
487
this.start = start;
488
this.end = end;
489
}
490
491
public int getStart() { return start; }
492
public int getEnd() { return end; }
493
}
494
495
// Usage would require custom expression parsing or function registration
496
StandardEvaluationContext context = new StandardEvaluationContext();
497
context.addIndexAccessor(new RangeIndexAccessor());
498
499
// This would require extending the parser or using functions
500
// Example conceptual usage: list[range(0, 2)] -> first 3 elements
501
```
502
{ .api }
503
504
## Accessor Chain Management
505
506
### Accessor Priority and Ordering
507
508
```java
509
public class AccessorManager {
510
511
public static StandardEvaluationContext createOptimizedContext() {
512
StandardEvaluationContext context = new StandardEvaluationContext();
513
514
// Add accessors in priority order (most specific first)
515
context.addPropertyAccessor(new MapAccessor()); // Maps
516
context.addPropertyAccessor(new DataBindingPropertyAccessor()); // Optimized
517
context.addPropertyAccessor(new ReflectivePropertyAccessor()); // General
518
519
context.addIndexAccessor(new ReflectiveIndexAccessor()); // General indexing
520
521
return context;
522
}
523
524
public static void configureForSecurity(StandardEvaluationContext context) {
525
// Clear default accessors and add only safe ones
526
context.setPropertyAccessors(Arrays.asList(
527
new SafePropertyAccessor(),
528
new WhitelistPropertyAccessor()
529
));
530
531
context.setIndexAccessors(Arrays.asList(
532
new SafeIndexAccessor()
533
));
534
}
535
}
536
537
public class SafePropertyAccessor implements PropertyAccessor {
538
private final Set<Class<?>> allowedTypes = Set.of(
539
String.class, Number.class, Boolean.class, Date.class
540
);
541
542
@Override
543
public Class<?>[] getSpecificTargetClasses() {
544
return allowedTypes.toArray(new Class<?>[0]);
545
}
546
547
@Override
548
public boolean canRead(EvaluationContext context, Object target, String name) {
549
return allowedTypes.contains(target.getClass()) &&
550
isAllowedProperty(name);
551
}
552
553
private boolean isAllowedProperty(String name) {
554
// Whitelist approach
555
return name.matches("^[a-zA-Z][a-zA-Z0-9]*$") && // Simple names only
556
name.length() < 50; // Reasonable length
557
}
558
559
// ... implement other methods with safety checks
560
}
561
```
562
{ .api }
563
564
## Performance Optimization
565
566
### Cached Accessor Results
567
568
```java
569
public class CachingPropertyAccessor implements PropertyAccessor {
570
private final PropertyAccessor delegate;
571
private final Map<String, TypedValue> readCache = new ConcurrentHashMap<>();
572
private final long cacheTimeout;
573
private final Map<String, Long> cacheTimestamps = new ConcurrentHashMap<>();
574
575
public CachingPropertyAccessor(PropertyAccessor delegate, long cacheTimeoutMs) {
576
this.delegate = delegate;
577
this.cacheTimeout = cacheTimeoutMs;
578
}
579
580
@Override
581
public Class<?>[] getSpecificTargetClasses() {
582
return delegate.getSpecificTargetClasses();
583
}
584
585
@Override
586
public boolean canRead(EvaluationContext context, Object target, String name) {
587
return delegate.canRead(context, target, name);
588
}
589
590
@Override
591
public TypedValue read(EvaluationContext context, Object target, String name)
592
throws AccessException {
593
String cacheKey = target.getClass().getName() + ":" + name;
594
Long timestamp = cacheTimestamps.get(cacheKey);
595
596
if (timestamp != null &&
597
System.currentTimeMillis() - timestamp < cacheTimeout) {
598
TypedValue cached = readCache.get(cacheKey);
599
if (cached != null) {
600
return cached;
601
}
602
}
603
604
TypedValue result = delegate.read(context, target, name);
605
readCache.put(cacheKey, result);
606
cacheTimestamps.put(cacheKey, System.currentTimeMillis());
607
608
return result;
609
}
610
611
@Override
612
public boolean canWrite(EvaluationContext context, Object target, String name) {
613
return delegate.canWrite(context, target, name);
614
}
615
616
@Override
617
public void write(EvaluationContext context, Object target, String name, Object newValue)
618
throws AccessException {
619
delegate.write(context, target, name, newValue);
620
621
// Invalidate cache on write
622
String cacheKey = target.getClass().getName() + ":" + name;
623
readCache.remove(cacheKey);
624
cacheTimestamps.remove(cacheKey);
625
}
626
}
627
```
628
{ .api }
629
630
### Specialized Accessors for Performance
631
632
```java
633
// Highly optimized accessor for specific data structures
634
public class OptimizedDataObjectAccessor implements CompilablePropertyAccessor {
635
636
@Override
637
public Class<?>[] getSpecificTargetClasses() {
638
return new Class<?>[] { OptimizedDataObject.class };
639
}
640
641
@Override
642
public boolean canRead(EvaluationContext context, Object target, String name) {
643
return target instanceof OptimizedDataObject;
644
}
645
646
@Override
647
public TypedValue read(EvaluationContext context, Object target, String name)
648
throws AccessException {
649
OptimizedDataObject obj = (OptimizedDataObject) target;
650
651
// Use optimized access methods
652
return switch (name) {
653
case "id" -> new TypedValue(obj.getId());
654
case "name" -> new TypedValue(obj.getName());
655
case "status" -> new TypedValue(obj.getStatus());
656
default -> throw new AccessException("Unknown property: " + name);
657
};
658
}
659
660
@Override
661
public boolean canWrite(EvaluationContext context, Object target, String name) {
662
return target instanceof OptimizedDataObject &&
663
!"id".equals(name); // ID is read-only
664
}
665
666
@Override
667
public void write(EvaluationContext context, Object target, String name, Object newValue)
668
throws AccessException {
669
OptimizedDataObject obj = (OptimizedDataObject) target;
670
671
switch (name) {
672
case "name" -> obj.setName((String) newValue);
673
case "status" -> obj.setStatus((String) newValue);
674
default -> throw new AccessException("Cannot write property: " + name);
675
}
676
}
677
678
// Compilation support for even better performance
679
@Override
680
public boolean isCompilable() {
681
return true;
682
}
683
684
@Override
685
public String generateCode(String propertyName, String target, CodeFlow cf) {
686
// Generate bytecode for direct property access
687
return switch (propertyName) {
688
case "id" -> target + ".getId()";
689
case "name" -> target + ".getName()";
690
case "status" -> target + ".getStatus()";
691
default -> null;
692
};
693
}
694
}
695
```
696
{ .api }
697
698
## Best Practices
699
700
### Security Considerations
701
702
```java
703
// 1. Use whitelisting for property names
704
public class SecurePropertyAccessor implements PropertyAccessor {
705
private final Set<String> allowedProperties;
706
private final PropertyAccessor delegate;
707
708
public SecurePropertyAccessor(PropertyAccessor delegate, String... allowedProperties) {
709
this.delegate = delegate;
710
this.allowedProperties = Set.of(allowedProperties);
711
}
712
713
@Override
714
public boolean canRead(EvaluationContext context, Object target, String name) {
715
return allowedProperties.contains(name) &&
716
delegate.canRead(context, target, name);
717
}
718
719
// ... implement other methods with security checks
720
}
721
722
// 2. Validate input types and ranges
723
public class ValidatingIndexAccessor implements IndexAccessor {
724
private final IndexAccessor delegate;
725
726
@Override
727
public TypedValue read(EvaluationContext context, Object target, Object index)
728
throws AccessException {
729
validateIndex(index);
730
return delegate.read(context, target, index);
731
}
732
733
private void validateIndex(Object index) throws AccessException {
734
if (index instanceof Integer) {
735
int idx = (Integer) index;
736
if (idx < 0 || idx > 10000) { // Reasonable bounds
737
throw new AccessException("Index out of safe range: " + idx);
738
}
739
}
740
}
741
}
742
```
743
{ .api }
744
745
### Performance Tips
746
747
1. **Order accessors by specificity**: Place most specific accessors first in the chain
748
2. **Use targeted accessors**: Implement `getSpecificTargetClasses()` to avoid unnecessary checks
749
3. **Cache expensive operations**: Implement caching for costly property access
750
4. **Consider compilation**: Implement `CompilablePropertyAccessor` for hot paths
751
5. **Minimize allocations**: Reuse `TypedValue` instances when possible
752
6. **Profile accessor chains**: Monitor which accessors are called most frequently
753
754
### Error Handling
755
756
```java
757
public class RobustPropertyAccessor implements PropertyAccessor {
758
private final PropertyAccessor delegate;
759
private final boolean failSilently;
760
761
public RobustPropertyAccessor(PropertyAccessor delegate, boolean failSilently) {
762
this.delegate = delegate;
763
this.failSilently = failSilently;
764
}
765
766
@Override
767
public TypedValue read(EvaluationContext context, Object target, String name)
768
throws AccessException {
769
try {
770
return delegate.read(context, target, name);
771
} catch (Exception e) {
772
if (failSilently) {
773
return TypedValue.NULL; // Return null instead of throwing
774
}
775
throw new AccessException("Failed to read property '" + name + "'", e);
776
}
777
}
778
779
// ... implement other methods with similar error handling
780
}
781
```
782
{ .api }