javaspring-bootspring-aopspring-annotationsspring-el

SpringAOP JoinPoint Arguments Parser with SpEL Expression


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 ?


Solution

  • 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.