[Github] 4. Value-Checker-Java: Customizable AOP Validation Framework
1. Introduction
Value-Checker-Java is essentially a customizable AOP pointcut framework. It allows developers to insert custom validation logic before method execution, and this validation logic can be arbitrarily complex business rules.
However, if it merely provides an AOP pointcut, that wouldn’t be very meaningful. The core value of Value-Checker-Java lies in its thread-safe context management mechanism. Without this context management, data queried in the first validator cannot be used in subsequent validators, forcing each validator to re-query data, which defeats the purpose of validation chains.
It is precisely because of ValueCheckerReentrantThreadLocal
, this thread-safe context manager, that multiple validators can share data and form truly meaningful validation chains.
2. Basic Usage
2.1 Validator Configuration
1
2
3
4
5
6
7
8
9
| // From TargetService.java
@ValueCheckers(checkers = {
@ValueCheckers.ValueChecker(method = "verify", keys = {"#id", "#name"}, handler = SampleCheckerHandlerImpl.class),
@ValueCheckers.ValueChecker(method = "verify", keys = "#id", handler = SampleCheckerHandlerImpl.class),
@ValueCheckers.ValueChecker(method = "verify", keys = "#name", handler = SampleCheckerHandlerImpl.class)
})
public void checker(Long id, String name) {
// Will execute 3 validators in sequence
}
|
2.2 Validator Implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // From SampleCheckerHandlerImpl.java
@Service
public class SampleCheckerHandlerImpl implements IValueCheckerHandler {
public static final Long CORRECT_ID = 2L;
public static final String CORRECT_NAME = "correctName";
public void verify(Long id, String name) {
if (!CORRECT_ID.equals(id) || !CORRECT_NAME.equals(name)) {
throw new ValueIllegalException("error");
}
}
public void verify(Long id) {
if (!CORRECT_ID.equals(id)) {
throw new ValueIllegalException("error");
}
}
public void verify(String name) {
if (!CORRECT_NAME.equals(name)) {
throw new ValueIllegalException("error");
}
}
}
|
2.3 Key Technical Implementation
2.3.1 Annotation Design
@ValueCheckers
adopts a nested annotation design pattern:
1
2
3
4
5
6
7
8
9
10
11
12
| @Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValueCheckers {
ValueChecker[] checkers();
@interface ValueChecker {
Class<? extends IValueCheckerHandler> handler();
String method() default "verify";
String[] keys() default "";
}
}
|
Design highlights:
- Array configuration: Supports multiple validator combinations
- Type safety: Handler must implement IValueCheckerHandler interface
- Flexible method mapping: Can specify any validation method name
- Parameterized configuration: Pass required parameters through keys array
2.3.2 SpEL Expression Engine
SeplUtil
class provides powerful parameter extraction capabilities:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public static Object[] getValue(ProceedingJoinPoint point, String[] keys) {
MethodSignature methodSignature = (MethodSignature) point.getSignature();
String[] params = methodSignature.getParameterNames();
Object[] args = point.getArgs();
EvaluationContext context = new StandardEvaluationContext();
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], args[len]);
}
Object[] values = new Object[keys.length];
for (int i = 0; i < keys.length; i++) {
Expression expression = SPEL_PARSER.parseExpression(keys[i]);
values[i] = expression.getValue(context, Object.class);
}
return values;
}
|
Technical features:
- Dynamic parameter mapping: Runtime acquisition of method parameter names
- Expression parsing: Supports complex SpEL expressions
- Type safety: Automatic type conversion handling
- Performance optimization: Reuses SpEL parser instances
2.3.3 Intelligent Method Invocation Mechanism
The method invocation mechanism in ValueCheckerAspect
has the following characteristics:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| private void methodInvoke(Object instance, String method, Object[] paras) {
// Generate method signature cache key
final String parasName = objectTypeName(paras);
final String objectMethodName = String.format(OBJECT_METHOD_FORMAT,
instanceClass.getSimpleName(), method, parasName);
// Prioritize cached methods
if (OBJECT_METHOD_MAP.containsKey(objectMethodName)) {
final Method pointMethod = OBJECT_METHOD_MAP.get(objectMethodName);
pointMethod.invoke(instance, paras);
return;
}
// First call: perform method matching and caching
for (Method subMethod : instanceClass.getMethods()) {
// Method name + parameter length + parameter type matching
if (subMethod.getName().equals(method) &&
subMethod.getParameterTypes().length == paras.length &&
parasName.equals(methodTypeName(subMethod.getParameterTypes()))) {
OBJECT_METHOD_MAP.put(objectMethodName, subMethod);
subMethod.invoke(instance, paras);
return;
}
}
}
|
Core advantages:
- Performance optimization: Method reflection result caching, avoiding repeated lookups
- Precise matching: Supports accurate identification of method overloading
- Type safety: Strict parameter type matching
2.3.4 Reentrant ThreadLocal Design
ValueCheckerReentrantThreadLocal
is an innovative design of the framework:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public static void init() {
final AtomicInteger counter = VALUE_CHECKER_THREAD_LOCAL_COUNTER.get();
if (null == counter) {
VALUE_CHECKER_THREAD_LOCAL.set(new ConcurrentHashMap<>());
VALUE_CHECKER_THREAD_LOCAL_COUNTER.set(new AtomicInteger());
return;
}
counter.addAndGet(1);
}
public static void clear() {
final AtomicInteger counter = VALUE_CHECKER_THREAD_LOCAL_COUNTER.get();
if (null == counter || counter.get() <= 0) {
VALUE_CHECKER_THREAD_LOCAL.remove();
VALUE_CHECKER_THREAD_LOCAL_COUNTER.remove();
return;
}
counter.addAndGet(-1);
}
|
Design essence:
- Reference counting: Uses AtomicInteger to implement reentrant counting
- Thread safety: ConcurrentHashMap ensures concurrent safety
- Automatic cleanup: Automatically cleans up resources when outermost call ends
- Nested support: Perfect support for nested validator calls
3. ThreadLocal Context Management
This is one of the core capabilities of the framework.
3.1 Data Storage and Retrieval
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // Store data to ThreadLocal
public void verifyPutThreadValue(String name) {
ValueCheckerReentrantThreadLocal.getOrDefault(String.class, name);
if (!ValueCheckerReentrantThreadLocal.getOrDefault(String.class, "").equals(name)) {
throw new ValueIllegalException("error");
}
}
// Retrieve data from ThreadLocal
public void verifyGetRightThreadValue(String name) {
if (!ValueCheckerReentrantThreadLocal.getOrDefault(String.class, name).equals(name)) {
throw new ValueIllegalException("error");
}
}
|
3.2 Why It’s Important
Without ThreadLocal:
1
2
3
4
5
6
7
| public void validateUser(Long userId) {
User user = userRepository.findById(userId); // 1st query
}
public void validateUserPermission(Long userId) {
User user = userRepository.findById(userId); // 2nd query, duplicate!
}
|
With ThreadLocal:
1
2
3
4
5
6
7
8
| public void validateUser(Long userId) {
User user = userRepository.findById(userId); // Only query once
ValueCheckerReentrantThreadLocal.put(user);
}
public void validateUserPermission(Long userId) {
User user = ValueCheckerReentrantThreadLocal.get(User.class, () -> null); // Direct retrieval
}
|
4. Reentrant Support
4.1 Test Scenario
1
2
3
4
5
6
7
8
9
| // From TargetService.java
@ValueCheckers(checkers = {
@ValueCheckers.ValueChecker(method = "verifyPutThreadValue", keys = "#name", handler = SampleCheckerHandlerImpl.class)
})
public void checkerReentrant(String name) {
// 1st layer AOP: store data to ThreadLocal
this.targetService.checkerGetThreadValue(name); // 2nd layer AOP
this.targetService.checkerGetWrongThreadValue(""); // 3rd layer AOP
}
|
4.2 Reentrant Counter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // ValueCheckerReentrantThreadLocal.java
public static void init() {
final AtomicInteger counter = VALUE_CHECKER_THREAD_LOCAL_COUNTER.get();
if (null == counter) {
// First call: initialize ThreadLocal
VALUE_CHECKER_THREAD_LOCAL.set(new ConcurrentHashMap<>());
VALUE_CHECKER_THREAD_LOCAL_COUNTER.set(new AtomicInteger());
} else {
// Nested call: counter+1
counter.addAndGet(1);
}
}
public static void clear() {
final AtomicInteger counter = VALUE_CHECKER_THREAD_LOCAL_COUNTER.get();
if (null == counter || counter.get() <= 0) {
// Outermost call: actually clean ThreadLocal
VALUE_CHECKER_THREAD_LOCAL.remove();
VALUE_CHECKER_THREAD_LOCAL_COUNTER.remove();
} else {
// Inner call: counter-1
counter.addAndGet(-1);
}
}
|
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // ValueCheckerAspectTest.java - Test comments explain the entire flow
// aop1 - 1.1 counter = 0 init ThreadLocal
// aop1 - 1.2 counter = 0 set RIGHT_VALUE to ThreadLocal
// aop2 - 2.1 counter = 1 init ThreadLocal
// aop2 - 2.2 counter = 1 try to set RIGHT_VALUE to ThreadLocal (success)
// aop2 - 2.3 counter = 0 clear ThreadLocal (if not reentrant, RIGHT_VALUE will be clear)
// aop3 - 3.1 counter = 1 init ThreadLocal
// aop3 - 3.2 counter = 1 try to set WRONG_VALUE to ThreadLocal (fail)
// aop3 - 3.3 counter = 0 clear ThreadLocal
// aop1 - 1.3 counter = null clear ThreadLocal
|
Without reentrant support, the second and third layer AOP calls would clear ThreadLocal data stored by the first layer, causing validation failure.
5. Core Architecture
5.1 AOP Aspect try-finally Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // ValueCheckerAspect.java - Key try-finally implementation
@Around("handleValueCheckerPoint() && @annotation(valueCheckers)")
public Object around(ProceedingJoinPoint point, ValueCheckers valueCheckers) throws Throwable {
try {
// init ThreadLocal, if init in sub ValueChecker, ThreadLocal will counter++
ValueCheckerReentrantThreadLocal.init();
for (ValueCheckers.ValueChecker checker : valueCheckers.checkers()) {
valueCheck(checker, point);
}
return point.proceed();
} finally {
// clear ThreadLocal, if clear in sub ValueChecker, ThreadLocal will counter--
ValueCheckerReentrantThreadLocal.clear();
}
}
|
5.2 Execution Flow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @ValueCheckers annotated method
↓
ValueCheckerAspect intercepts
↓
try {
init ThreadLocal (reentrant counting)
↓
iterate checkers array
↓
SpEL parameter extraction → reflection call Handler
↓
validation fails throw exception / validation succeeds continue
↓
all validations pass execute original method
} finally {
clear ThreadLocal (reentrant counting)
}
|
Key roles of try-finally:
- Guaranteed resource cleanup: ThreadLocal is cleaned regardless of validation success or failure
- Reentrant count management: Ensures proper ThreadLocal management in nested calls through counter
- Memory leak prevention: Ensures ThreadLocal is properly cleaned when method ends
6.1 Method Caching Mechanism
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // ValueCheckerAspect.java - Method caching based on performance considerations
private static final ConcurrentHashMap<String, Method> OBJECT_METHOD_MAP = new ConcurrentHashMap<>();
private void methodInvoke(Object instance, String method, Object[] paras) {
// Generate cache key
final String objectMethodName = String.format(OBJECT_METHOD_FORMAT,
instanceClass.getSimpleName(), method, objectTypeName(paras));
// Prioritize cache usage
if (OBJECT_METHOD_MAP.containsKey(objectMethodName)) {
final Method pointMethod = OBJECT_METHOD_MAP.get(objectMethodName);
pointMethod.invoke(instance, paras);
return;
}
// First call: traverse methods and cache
for (Method subMethod : instanceClass.getMethods()) {
if (subMethod.getName().equals(method) &&
subMethod.getParameterTypes().length == paras.length &&
objectTypeName(paras).equals(methodTypeName(subMethod.getParameterTypes()))) {
OBJECT_METHOD_MAP.put(objectMethodName, subMethod); // Cache result
subMethod.invoke(instance, paras);
return;
}
}
}
|
Reflection overhead optimization:
- First call traverses all methods for matching
- Subsequent calls directly get Method object from ConcurrentHashMap
- Avoids repeated reflection lookup operations
SpEL expression performance:
- Reuses SpEL parser instance:
private static final ExpressionParser SPEL_PARSER
- Runtime parameter mapping, supports complex expressions but has performance cost
ThreadLocal overhead:
- ThreadLocal operations themselves are lightweight
- Reentrant counter uses AtomicInteger, thread-safe and efficient
Validation chain execution:
- Multiple validators execute serially
- Total time = sum of individual validator times
- Reduces duplicate queries through data sharing
7. Java 8 vs Java 17
Core difference: SpEL parameter name acquisition
Java 17 requires additional configuration:
1
2
3
4
5
6
7
| <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<parameters>true</parameters> <!-- Preserve parameter names -->
</configuration>
</plugin>
|
Dependency versions:
- Java 8: Spring Boot 2.5.13
- Java 17: Spring Boot 3.5.5
8. Summary
Value-Checker-Java solves the core problem: data sharing between validators.
- Essence: Customizable AOP + ThreadLocal context
- Value: Avoid duplicate queries, make validation chains meaningful
- Key: Reentrant ThreadLocal supports nested calls
- Scenario: Multi-step validation that needs to share query results