Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Intercepting and Parsing Field-Level Custom Annotations on Method Parameters in Spring Boot

Tech May 15 1

Technical Approach

Parsing annotations on class fields is straightforward. The approach used here involves:

Creating a custom MethodInterceptor that intercepts the target business methods. It retrieves the array of method arguments, iterates over each argument, inspects all fields of each argument object, checks for the presence of the custom annotation on each field, collects the annotated fields, and then applies the desired business logic.

Implementation

1. Requirement

Define a custom annotation that can be placed on class fields. At runtime, if an annotatde field has not been explicitly set to a non‑null value, the field should be populated with the default value provided by the annotation.

2. Project Setup

Create a Spring Boot project with the following dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

3. Custom Annotation

Define an annotation that targets fields:

import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FieldDefault {
    String fallback() default "";
}

4. Annotation Extraction Utility

This utility scans an object for fields annotated with a givan annotation and, optionally, filters by field type:

import lombok.AllArgsConstructor;
import lombok.Getter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

public class FieldResolver {

    @Getter
    @AllArgsConstructor
    public static class AnnotatedField {
        private final Object target;
        private final Field field;
        private final Annotation annotation;
    }

    public static List<AnnotatedField> findAnnotatedFields(Object target,
                                                             Class<? extends Annotation> annotationClass,
                                                             Set<Class<?>> eligibleTypes) {
        if (target == null) {
            return Collections.emptyList();
        }
        return Arrays.stream(target.getClass().getDeclaredFields())
                .filter(field -> field.isAnnotationPresent(annotationClass))
                .filter(field -> eligibleTypes == null || eligibleTypes.isEmpty() ||
                        eligibleTypes.stream().anyMatch(c -> c.isAssignableFrom(field.getType())))
                .map(field -> new AnnotatedField(target, field, field.getAnnotation(annotationClass)))
                .collect(Collectors.toList());
    }
}

5. Custom MethodInterceptor

The interceptor uses the utility to discover annotated fields and sets defaults when values are absent:

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.util.CollectionUtils;
import java.lang.reflect.Field;
import java.util.*;

public class FieldInitializerInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Object[] arguments = invocation.getArguments();
        if (arguments != null) {
            Set<Class<?>> allowedTypes = new HashSet<>(Collections.singletonList(CharSequence.class));
            for (Object arg : arguments) {
                List<FieldResolver.AnnotatedField> annotatedFields =
                        FieldResolver.findAnnotatedFields(arg, FieldDefault.class, allowedTypes);
                if (!CollectionUtils.isEmpty(annotatedFields)) {
                    for (FieldResolver.AnnotatedField af : annotatedFields) {
                        Field field = af.getField();
                        field.setAccessible(true);
                        Object currentValue = field.get(af.getTarget());
                        if (currentValue == null) {
                            FieldDefault ann = (FieldDefault) af.getAnnotation();
                            field.set(af.getTarget(), ann.fallback());
                        }
                        field.setAccessible(false);
                    }
                }
            }
        }
        return invocation.proceed();
    }
}

6. Registering the Interceptor

A configuration class weaves the interceptor into the Spring container with a pointcut expression:

import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class InterceptorAdvisorConfig {

    @Bean
    public DefaultPointcutAdvisor fieldInitializerAdvisor() {
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
        advisor.setAdvice(new FieldInitializerInterceptor());

        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* com.learn..*(..))");
        advisor.setPointcut(pointcut);

        return advisor;
    }
}

7. Annotated Data Object

A simple POJO with fields carrying the custom annotation:

import lombok.Data;

@Data
public class ParameterDto {

    @FieldDefault(fallback = "我是注解设置的值.")
    private String name;

    @FieldDefault(fallback = "100")
    private Integer age;
}

8. Service with Overloaded Methods

The service exposes methods that accept the annotated object as a parameter:

import org.springframework.stereotype.Component;

@Component
public class SampleService {

    public void execute() {
        System.out.println("execute empty...");
    }

    public void execute(ParameterDto dto) {
        System.out.println("execute dto: " + dto.toString());
    }

    public void execute(ParameterDto dto, String extra) {
        System.out.println("execute dto: " + dto.toString() + " extra: " + extra);
    }
}

9. Verification

  • Test runner:

    @Autowired
    private SampleService sampleService;
    
    @PostConstruct
    public void init() {
        System.out.println("=========================");
        sampleService.execute();
        ParameterDto dto = new ParameterDto();
        sampleService.execute(dto);
        sampleService.execute(dto, null);
        dto.setName("我是自己设置的...");
        sampleService.execute(dto, "hello, world.");
        System.out.println("=========================");
    }
    
    
  • Console output:

    Test result showing default values from annotations being applied

  • Observations

    When a field is left unset, the default value declared in the annotation is used. Fields whose types are not included in the allowed set remain unchanged, even if they carry the annotation.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.