Elegant Parameter Annotation + AOP for Shop Permission Validation in Spring

The article demonstrates how to create a custom @ValidShop annotation and an AOP aspect that intercepts Spring MVC controller methods, extracts the annotated parameter via reflection, validates the shop code through a helper service, and proceeds only when the check succeeds.

Architect's Journey
Architect's Journey
Architect's Journey
Elegant Parameter Annotation + AOP for Shop Permission Validation in Spring

Requirement Background

To prevent a merchant from performing unauthorized operations on other stores after logging into the merchant center, the shop account (identified by shopCode) must be validated by calling a shop‑service API.

Technology and Principles Involved

The solution relies on a custom annotation combined with Spring AOP, which internally uses dynamic proxies and Java reflection. This pattern can also be applied to logging, distributed locks, flow control, transaction management, and ThreadLocal‑based implicit parameter passing.

Custom Annotation Definition

import java.lang.annotation.*;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidShop {
    String value() default "shopCode";
}

AOP Interceptor Implementation

import org.apache.commons.lang.ArrayUtils;
import org.apache.logging.log4j.util.Strings;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Optional;

@Aspect
@Component
public class ShopValidator {

    @Around("@within(org.springframework.web.bind.annotation.RestController)")
    public Object aroundController(ProceedingJoinPoint pjp) throws Throwable {
        Optional<String> shopCode = getAnnotatedValue(pjp);
        if (shopCode.isPresent()) {
            Result validateResult = ShopHelper.validateShopCode(shopCode.get());
            if (validateResult.isFailed()) {
                return validateResult;
            }
        }
        return pjp.proceed();
    }

    private static Optional<String> getAnnotatedValue(ProceedingJoinPoint pjp)
            throws NoSuchFieldException, IllegalAccessException {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations();
        for (Annotation[] parameterAnnotation : parameterAnnotations) {
            int paramIndex = ArrayUtils.indexOf(parameterAnnotations, parameterAnnotation);
            for (Annotation annotation : parameterAnnotation) {
                if (annotation instanceof ValidShop) {
                    ValidShop annotated = (ValidShop) annotation;
                    Object[] args = pjp.getArgs();
                    Object param = args[paramIndex];
                    if (param != null) {
                        if (param instanceof String) {
                            return Optional.of((String) param);
                        } else {
                            String fieldName = annotated.value();
                            Field field = param.getClass().getDeclaredField(fieldName);
                            field.setAccessible(true);
                            Object fieldValue = field.get(param);
                            if (fieldValue instanceof String) {
                                return Optional.of((String) fieldValue);
                            }
                        }
                    }
                    return Optional.of(Strings.EMPTY);
                }
            }
        }
        return Optional.empty();
    }
}

Controller Method Usage

/** Method 1: receive shopCode as String */
@GetMapping("/{shopCode}")
public Result<Void> xxx(@PathVariable("shopCode") @ValidShop String shopCode) {
    return Result.ok();
}

/** Method 2: receive object containing shopCode */
@PostMapping("/page")
public Result<Void> xxx(@RequestBody @ValidShop ShopQueryParam param) {
    return Result.ok();
}

The core idea is to obtain the method parameter annotated with @ValidShop via reflection, retrieve its value (or the value of the specified field when the parameter is an object), and invoke the business validation logic. If validation fails, the aspect returns the failure result directly; otherwise, the original controller method proceeds.

This approach achieves low‑intrusion code enhancement, decouples validation logic from business code, and can be extended to other cross‑cutting concerns such as logging, distributed locking, and flow control.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaAOPBackend DevelopmentSpringCustom AnnotationParameter Validation
Architect's Journey
Written by

Architect's Journey

E‑commerce, SaaS, AI architect; DDD enthusiast; SKILL enthusiast

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.