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!