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.
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-
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.