Intercepting and Parsing Field-Level Custom Annotations on Method Parameters in Spring Boot
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:

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