I'm creating an annotation, like below
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Annotation {
String template();
String[] parameters() default {};
}
I want it to be a generic one, so I can use it with nested objects or simple integers.
I will use it on top of methods, like
@Annotation(
template = "Test-{0}-{1}-{2}",
parameters = {"#request.playroundUid.fundraiserId", "#request.selectionPlayroundNumber", "#request.playroundUid.playroundType"}
)
public TestResponse test(Request request)
So I created an aspect which is using the method below
public static String generateName(ProceedingJoinPoint joinPoint, Annotation annotation) {
String template = annotation.template();
Object[] args = joinPoint.getArgs();
String[] parameters = annotation.parameters();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
// Create a map of parameter names -> values
Map<String, Object> paramMap = new HashMap<>();
for (int i = 0; i < parameterNames.length; i++) {
paramMap.put(parameterNames[i], args[i]);
}
// DEBUG: Print out the map to verify correct parameter storage
System.out.println("Parameter Map: " + paramMap);
// SpEL context
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariables(paramMap); // Bind method parameters (e.g., request)
ExpressionParser parser = new SpelExpressionParser();
for (int i = 0; i < parameters.length; i++) {
String parameter = parameters[i];
if (parameter.startsWith("#")) {
try {
String expression = parameter.substring(1); // Remove "#"
// DEBUG: Print out the expression being evaluated
System.out.println("Evaluating SpEL expression: " + expression);
// Evaluate the SpEL expression directly
Object evaluatedValue = parser.parseExpression(expression).getValue(context);
if (evaluatedValue == null) {
throw new RuntimeException("SpEL expression '" + parameter + "' evaluated to null. Check field access.");
}
template = template.replace("{" + i + "}", evaluatedValue.toString());
} catch (Exception e) {
throw new RuntimeException("Failed to evaluate SpEL expression: " + parameter, e);
}
} else {
template = template.replace("{" + i + "}", args[i] != null ? args[i].toString() : "null");
}
}
return template;
}
But the line below fails to parse request object.
parser.parseExpression(expression).getValue(context);
java.lang.RuntimeException: Failed to evaluate SpEL expression: #request.playroundUid.fundraiserId
at com.novamedia.nl.beehive.participationadministration.util.AnnotationUtil.generateName(AnnotationUtil.java:112)
at com.novamedia.nl.beehive.participationadministration.aspect.AspectTest.shouldGenerateCorrectNameForComplexParameters(AspectTest.java:194)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596) Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1007E: Property or field 'request' cannot be found on null
at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:225)
at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:112)
at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:100)
at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:60)
at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:96)
at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:114)
at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:273)
at com.novamedia.nl.beehive.participationadministration.util.AnnotationUtil.generateLockName(AnnotationUtil.java:104)
... 4 more
Here is my unit test
@Test
void shouldGenerateCorrectNameForComplexParameters() throws NoSuchMethodException {
// Arrange
Annotation annotation = mock(Annotation.class);
when(annotation.template()).thenReturn("CreateTicketsForPlayround-{0}-{1}-{2}");
when(annotation.parameters()).thenReturn(new String[]{"#request.playroundUid.fundraiserId", "#request.selectionPlayroundNumber", "#request.playroundUid.playroundType"});
Method mockMethod = MyTestService.class.getMethod("createTicketsForPlayround", CreateTicketsForPlayroundRequest.class);
when(methodSignature.getMethod()).thenReturn(mockMethod);
when(methodSignature.getParameterNames()).thenReturn(new String[]{"request"});
// Create a mock request with nested properties
CreateTicketsForPlayroundRequest request = CreateTicketsForPlayroundRequest.builder()
.selectionPlayroundNumber(1)
.playroundUid(new PlayroundUid(1001, 1, PlayroundType.REGULAR, 1))
.build();
when(joinPoint.getArgs()).thenReturn(new Object[]{request});
// Act
String name = generateName(joinPoint, annotation);
// Assert
assertEquals("CreateTicketsForPlayround-1001-1-REGULAR", name);
}
Briefly, my questions are; how can I parse request objects by using SpEL expressions ? what is the best practise for such case ? Should I use SpEL expressions or is there another way to handle ?
There is no problem with the SpelExpressionParser
but rather with the way you are using it. Or to be more precise the expression you are parsing.
if (parameter.startsWith("#")) {
try {
String expression = parameter.substring(1); // Remove "#"
// DEBUG: Print out the expression being evaluated
System.out.println("Evaluating SpEL expression: " + expression);
// Evaluate the SpEL expression directly
Object evaluatedValue = parser.parseExpression(expression).getValue(context);
if (evaluatedValue == null) {
throw new RuntimeException("SpEL expression '" + parameter + "' evaluated to null. Check field access.");
}
template = template.replace("{" + i + "}", evaluatedValue.toString());
} catch (Exception e) {
throw new RuntimeException("Failed to evaluate SpEL expression: " + parameter, e);
}
}
This bit contains the problematic part. You check if the expression starts with a #
and then remove the #
with the parameter.substring(1)
. This does actually change the expression and will interpret request
as being a property
/ field
of a root object. As there is no root object here it will throw an error as you get.
The fix is rather simple just use the expression as is instead of removing the #
.
if (parameter.startsWith("#")) {
try {
String expression = parameter
// DEBUG: Print out the expression being evaluated
System.out.println("Evaluating SpEL expression: " + expression);
// Evaluate the SpEL expression directly
Object evaluatedValue = parser.parseExpression(expression).getValue(context);
if (evaluatedValue == null) {
throw new RuntimeException("SpEL expression '" + parameter + "' evaluated to null. Check field access.");
}
template = template.replace("{" + i + "}", evaluatedValue.toString());
} catch (Exception e) {
throw new RuntimeException("Failed to evaluate SpEL expression: " + parameter, e);
}
}
Now that the expression starts with a #
it will properly resolve to a VariableReference
which will use your created Map
to lookup a variable named request
. The remained of the expression is then used as PropertyOrFieldReference
on the returned object.
TIP: The SpelExpressionParser
is thread-safe so you can use a single instance instead of recreating it each time you need it.