Backend Development 9 min read

Creating Custom Annotations for Validation, Permission, and Caching in Java Backend Development

This article explains how to design and implement custom Java annotations for field validation, permission checks, and caching, covering annotation definitions, target and retention policies, validator classes, interceptor and aspect implementations, and practical usage examples within Spring MVC.

Java Captain
Java Captain
Java Captain
Creating Custom Annotations for Validation, Permission, and Caching in Java Backend Development

Field Annotations

Field annotations are typically used to validate whether a field meets certain requirements. The hibernate-validator library provides many built‑in validation annotations such as @NotNull and @Range , but they cannot satisfy every business scenario.

When a parameter must belong to a specific String collection, existing annotations fall short, so a custom annotation is needed.

Custom Annotation

Define an annotation @Check using the @interface keyword:

@Target({ ElementType.FIELD }) // only on fields
@Retention(RetentionPolicy.RUNTIME) // retained at runtime for reflection
@Constraint(validatedBy = ParamConstraintValidated.class)
public @interface Check {
    /**
     * Allowed parameter values
     */
    String[] paramValues();

    /**
     * Message to display when validation fails
     */
    String message() default "Parameter is not an allowed value";

    Class
[] groups() default {};
    Class
[] payload() default {};
}

@Target specifies where the annotation can be placed (e.g., ElementType.FIELD for fields). @Retention defines the lifecycle of the annotation, with RetentionPolicy.RUNTIME allowing reflection at runtime.

@Constraint links the annotation to a validator class via the validatedBy attribute.

Validator Class

The validator must implement the generic ConstraintValidator interface:

public class ParamConstraintValidated implements ConstraintValidator
{
    /**
     * Valid values extracted from the annotation
     */
    private List
paramValues;

    @Override
    public void initialize(Check constraintAnnotation) {
        // Retrieve allowed values from the annotation
        paramValues = Arrays.asList(constraintAnnotation.paramValues());
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext context) {
        if (paramValues.contains(o)) {
            return true;
        }
        // Not in the allowed list
        return false;
    }
}

The first generic type is the custom annotation ( Check ), and the second is the type of the field being validated ( Object in this example). The initialize method reads annotation parameters, while isValid contains the validation logic.

Usage Example

Define an entity class and apply the custom annotation to a field:

@Data
public class User {
    /** Name */
    private String name;

    /** Gender: "man" or "woman" */
    @Check(paramValues = {"man", "woman"})
    private String sex;
}

When the sex field is validated, its value must be either "man" or "woman" .

Method and Class Annotations

Custom annotations can also be used on methods or classes to implement features such as permission checks or multi‑level caching (e.g., Guava → Redis → MySQL).

Permission Annotation

Custom Annotation

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionCheck {
    /** Resource key used for permission verification */
    String resourceKey();
}

This annotation can be placed on a class or a method.

Interceptor

public class PermissionCheckInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        PermissionCheck permission = findPermissionCheck(handlerMethod);
        if (permission == null) {
            return true; // No annotation, allow access
        }
        String resourceKey = permission.resourceKey();
        // Simple demo: allow only when resourceKey equals "testKey"
        if ("testKey".equals(resourceKey)) {
            return true;
        }
        return false;
    }

    private PermissionCheck findPermissionCheck(HandlerMethod handlerMethod) {
        PermissionCheck permission = handlerMethod.getMethodAnnotation(PermissionCheck.class);
        if (permission == null) {
            permission = handlerMethod.getBeanType().getAnnotation(PermissionCheck.class);
        }
        return permission;
    }
}

The interceptor checks for the presence of @PermissionCheck , extracts the resourceKey , and decides whether to allow the request.

Test Controller

@RestController("/api/test")
public class TestController {
    @GetMapping("/api/test")
    @PermissionCheck(resourceKey = "test")
    public Object testPermissionCheck() {
        return "hello world";
    }
}

The method is protected by the custom permission annotation.

Cache Annotation

Custom Annotation

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomCache {
    /** Cache key */
    String key();
}

Typically applied to methods.

Aspect

@Aspect
@Component
public class CustomCacheAspect {
    @Around("@annotation(com.cqupt.annotation.CustomCache) && @annotation(customCache)")
    public Object dealProcess(ProceedingJoinPoint pjd, CustomCache customCache) {
        if (customCache.key() == null) {
            // TODO: throw error
        }
        // Simple demo: if key equals "testKey" return a fixed value
        if ("testKey".equals(customCache.key())) {
            return "hello word";
        }
        // Execute the target method
        try {
            return pjd.proceed();
        } catch (Throwable t) {
            t.printStackTrace();
            return null;
        }
    }
}

The aspect processes the annotation before the method execution, returning a cached value when appropriate.

Test Method

@GetMapping("/api/cache")
@CustomCache(key = "test")
public Object testCustomCache() {
    return "don't hit cache";
}

If the key matches the cached entry, the aspect will return the cached response instead of invoking the method.

-END-

JavaSpringvalidationAspect-Oriented Programmingcustom annotations
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

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