Menu

Spring Boot Topic

Custom Annotations in Spring Boot

Custom annotations are a powerful feature in Spring Boot that allow you to define your own metadata and behavior. They help reduce boilerplate code and enable declarative programming.

1. Creating Custom Annotations

Basic Annotation Structure

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
    String value() default "";
}

Annotation Meta-annotations

  • @Target: Where the annotation can be applied

    • ElementType.METHOD, TYPE, FIELD, PARAMETER, CONSTRUCTOR

  • @Retention: How long the annotation is retained

    • RetentionPolicy.SOURCE, CLASS, RUNTIME

  • @Documented: Include in JavaDoc

  • @Inherited: Inherit by subclasses

2. Common Use Cases with Implementation

Use Case 1: Execution Time Logging

Annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
    String module() default "APPLICATION";
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

Aspect Implementation:

@Aspect
@Component
@Slf4j
public class LogExecutionTimeAspect {
    
    @Around("@annotation(logExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint, 
                                  LogExecutionTime logExecutionTime) throws Throwable {
        
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long executionTime = System.currentTimeMillis() - startTime;
            long convertedTime = convertTime(executionTime, logExecutionTime.timeUnit());
            
            log.info("{} - {} executed in {} {}",
                    logExecutionTime.module(),
                    joinPoint.getSignature().getName(),
                    convertedTime,
                    logExecutionTime.timeUnit().name().toLowerCase());
        }
    }
    
    private long convertTime(long millis, TimeUnit timeUnit) {
        switch (timeUnit) {
            case SECONDS: return TimeUnit.MILLISECONDS.toSeconds(millis);
            case MINUTES: return TimeUnit.MILLISECONDS.toMinutes(millis);
            case MICROSECONDS: return TimeUnit.MILLISECONDS.toMicros(millis);
            case NANOSECONDS: return TimeUnit.MILLISECONDS.toNanos(millis);
            default: return millis;
        }
    }
}

Usage:

@Service
public class UserService {
    
    @LogExecutionTime(module = "USER_SERVICE", timeUnit = TimeUnit.MILLISECONDS)
    public User getUserById(Long id) {
        // Simulate database call
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return new User(id, "John Doe");
    }
}

Use Case 2: Custom Security Annotation

Annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
    String[] permissions();
    Logical logical() default Logical.AND;
    
    enum Logical {
        AND, OR
    }
}

Aspect Implementation:

@Aspect
@Component
public class SecurityAspect {
    
    @Autowired
    private SecurityContext securityContext;
    
    @Before("@annotation(requirePermission)")
    public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
        String[] requiredPermissions = requirePermission.permissions();
        Logical logical = requirePermission.logical();
        
        boolean hasPermission;
        if (logical == Logical.AND) {
            hasPermission = Arrays.stream(requiredPermissions)
                    .allMatch(securityContext::hasPermission);
        } else {
            hasPermission = Arrays.stream(requiredPermissions)
                    .anyMatch(securityContext::hasPermission);
        }
        
        if (!hasPermission) {
            throw new AccessDeniedException(
                "User doesn't have required permissions: " + 
                Arrays.toString(requiredPermissions)
            );
        }
    }
}

Usage:

@RestController
public class UserController {
    
    @RequirePermission(permissions = {"USER_READ", "ADMIN_ACCESS"})
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUserById(id);
    }
    
    @RequirePermission(permissions = {"USER_WRITE", "USER_DELETE"}, logical = RequirePermission.Logical.OR)
    @DeleteMapping("/users/{id}")
    public void deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
    }
}

Use Case 3: Rate Limiting Annotation

Annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
    int value() default 100; // requests
    int windowInSeconds() default 60; // time window
    String key() default ""; // custom key for rate limiting
}

Aspect Implementation:

@Aspect
@Component
public class RateLimitAspect {
    
    private final Map<String, RateLimitBucket> buckets = new ConcurrentHashMap<>();
    
    @Around("@annotation(rateLimit)")
    public Object rateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        String key = generateKey(joinPoint, rateLimit);
        RateLimitBucket bucket = buckets.computeIfAbsent(key, 
            k -> new RateLimitBucket(rateLimit.value(), rateLimit.windowInSeconds()));
        
        if (!bucket.tryConsume()) {
            throw new RateLimitExceededException("Rate limit exceeded for key: " + key);
        }
        
        return joinPoint.proceed();
    }
    
    private String generateKey(ProceedingJoinPoint joinPoint, RateLimit rateLimit) {
        if (!rateLimit.key().isEmpty()) {
            return rateLimit.key();
        }
        
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        return String.format("%s.%s", 
            signature.getDeclaringTypeName(), 
            signature.getMethod().getName());
    }
    
    private static class RateLimitBucket {
        private final int maxRequests;
        private final long windowInMillis;
        private final Queue<Long> requests = new LinkedList<>();
        
        public RateLimitBucket(int maxRequests, int windowInSeconds) {
            this.maxRequests = maxRequests;
            this.windowInMillis = windowInSeconds * 1000L;
        }
        
        public synchronized boolean tryConsume() {
            long currentTime = System.currentTimeMillis();
            
            // Remove expired requests
            while (!requests.isEmpty() && 
                   requests.peek() < currentTime - windowInMillis) {
                requests.poll();
            }
            
            if (requests.size() < maxRequests) {
                requests.offer(currentTime);
                return true;
            }
            
            return false;
        }
    }
}

Usage:

@RestController
public class ApiController {
    
    @RateLimit(value = 10, windowInSeconds = 60) // 10 requests per minute
    @GetMapping("/public/api")
    public String publicApi() {
        return "Public API Response";
    }
    
    @RateLimit(value = 1000, windowInSeconds = 3600, key = "premium-users") // 1000 requests per hour
    @GetMapping("/premium/api")
    public String premiumApi() {
        return "Premium API Response";
    }
}

Use Case 4: Validation Annotation

Annotation:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ValidEmailValidator.class)
public @interface ValidEmail {
    String message() default "Invalid email format";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

Validator Implementation:

@Component
public class ValidEmailValidator implements ConstraintValidator<ValidEmail, String> {
    
    private static final String EMAIL_REGEX = "^[A-Za-z0-9+_.-]+@(.+)$";
    
    @Override
    public void initialize(ValidEmail constraintAnnotation) {
    }
    
    @Override
    public boolean isValid(String email, ConstraintValidatorContext context) {
        if (email == null) {
            return false;
        }
        return email.matches(EMAIL_REGEX);
    }
}

Usage:

@RestController
public class UserController {
    
    @PostMapping("/users")
    public User createUser(@RequestBody @Valid CreateUserRequest request) {
        return userService.createUser(request);
    }
}

@Data
class CreateUserRequest {
    @NotBlank
    private String name;
    
    @ValidEmail
    private String email;
    
    @Min(18)
    private int age;
}

Use Case 5: Caching Annotation

Annotation:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomCache {
    String cacheName();
    long ttl() default 300; // seconds
    String key() default "";
    CacheType type() default CacheType.LOCAL;
    
    enum CacheType {
        LOCAL, REDIS, MEMCACHED
    }
}

Aspect Implementation:

@Aspect
@Component
public class CustomCacheAspect {
    
    @Autowired
    private CacheManager cacheManager;
    
    @Around("@annotation(customCache)")
    public Object cacheResult(ProceedingJoinPoint joinPoint, CustomCache customCache) throws Throwable {
        String cacheName = customCache.cacheName();
        String key = generateKey(joinPoint, customCache);
        
        Cache cache = cacheManager.getCache(cacheName);
        Cache.ValueWrapper cachedValue = cache.get(key);
        
        if (cachedValue != null) {
            return cachedValue.get();
        }
        
        Object result = joinPoint.proceed();
        cache.put(key, result);
        
        return result;
    }
    
    private String generateKey(ProceedingJoinPoint joinPoint, CustomCache customCache) {
        if (!customCache.key().isEmpty()) {
            return customCache.key();
        }
        
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Object[] args = joinPoint.getArgs();
        
        return String.format("%s.%s:%s", 
            signature.getDeclaringTypeName(),
            signature.getMethod().getName(),
            Arrays.deepHashCode(args));
    }
}

3. Advanced Custom Annotation Features

Composed Annotations

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@LogExecutionTime(module = "SECURE_METHOD")
@RequirePermission(permissions = {"ADMIN_ACCESS"})
@RateLimit(value = 5, windowInSeconds = 60)
public @interface SecureAdminMethod {
    String description() default "";
}

Conditional Annotations

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnEnvironmentCondition.class)
public @interface ConditionalOnEnvironment {
    String value();
}

public class OnEnvironmentCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> attributes = metadata.getAnnotationAttributes(
            ConditionalOnEnvironment.class.getName());
        String expectedEnv = (String) attributes.get("value");
        String actualEnv = context.getEnvironment().getProperty("app.environment");
        return expectedEnv.equals(actualEnv);
    }
}

4. Configuration Setup

Enable AspectJ

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.yourpackage")
public class AppConfig {
    
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();
    }
    
    @Bean
    public SecurityContext securityContext() {
        return new SecurityContext();
    }
}

Maven Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

Custom annotations in Spring Boot provide a powerful way to implement cross-cutting concerns, reduce code duplication, and make your code more declarative and readable.

 

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?