Creating Custom Annotations in Spring Boot
Introduction
Annotations in Java and Spring Boot provide metadata that helps the compiler and runtime environment understand how to process code. While Spring Boot provides many built-in annotations, sometimes developers need custom annotations to enforce business logic, enhance readability, or introduce cross-cutting concerns like logging and security.
1. Understanding Annotations in Java and Spring Boot
Annotations in Java are a form of metadata that provide additional information about the code. They do not affect the actual execution of the code but help in code analysis, compilation, and runtime processing.
Spring Boot heavily relies on annotations such as:
• @Component, @Service, @Repository for defining Spring Beans
• @Autowired for dependency injection
• @Transactional for database transactions
• @RestController, @RequestMapping, @GetMapping for REST APIs
While these built-in annotations cover most use cases, developers sometimes require custom annotations to simplify and standardize specific tasks.
2. Why Create Custom Annotations?
Custom annotations in Spring Boot offer several advantages:
2.1 Code Reusability
Instead of duplicating the same logic across multiple methods or classes, a custom annotation encapsulates it, reducing redundancy.
2.2 Improved Readability and Maintainability
Annotations make code cleaner and more expressive. Developers can understand a method’s intent without digging into implementation details.
2.3 Cross-Cutting Concerns
Custom annotations can handle cross-cutting concerns like logging, validation, caching, security, or transaction management.
2.4 Decoupling Logic
Business logic can be separated from infrastructural concerns, making the code more modular.
3. Steps to Create and Use Custom Annotations in Spring Boot
Creating a custom annotation involves four steps:
1. Define the Annotation
2. Apply the Annotation
3. Process the Annotation
4. Test the Annotation
Step 1: Define the Custom Annotation
A custom annotation in Java is defined using @interface.
Annotation Elements
• @Target: Specifies where the annotation can be applied (methods, fields, classes, etc.).
• @Retention: Defines how long the annotation is retained (compile-time, class file, or runtime).
• @Inherited: Specifies whether subclasses inherit the annotation.
• @Documented: Indicates that the annotation should be included in JavaDocs.
4. Creating a Custom Annotation for Logging
4.1 Define the Custom Annotation
We create a custom annotation called @LogExecutionTime to measure the execution time of a method.
package com.example.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // Apply only to methods
@Retention(RetentionPolicy.RUNTIME) // Available at runtime
public @interface LogExecutionTime {
}
4.2 Implement the Annotation Processing with Aspect-Oriented Programming (AOP)
Spring AOP (Aspect-Oriented Programming) allows us to intercept method calls and add cross-cutting behavior.
package com.example.aspects;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogExecutionTimeAspect {
private static final Logger logger = LoggerFactory.getLogger(LogExecutionTimeAspect.class);
@Around("@annotation(com.example.annotations.LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed(); // Proceed with the method execution
long end = System.currentTimeMillis();
logger.info("{} executed in {} ms", joinPoint.getSignature(), (end - start));
return result;
}
}
4.3 Applying the Custom Annotation
We apply @LogExecutionTime to a method.
package com.example.services;
import com.example.annotations.LogExecutionTime;
import org.springframework.stereotype.Service;
@Service
public class SampleService {
@LogExecutionTime
public String performTask() throws InterruptedException {
Thread.sleep(1000); // Simulating a time-consuming task
return "Task completed!";
}
}
4.4 Testing the Custom Annotation
We create a REST Controller to test the annotation.
package com.example.controllers;
import com.example.services.SampleService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
private final SampleService sampleService;
public TestController(SampleService sampleService) {
this.sampleService = sampleService;
}
@GetMapping("/execution-time")
public String testExecutionTime() throws InterruptedException {
return sampleService.performTask();
}
}
When we hit http://localhost:8080/test/execution-time, the log will show:
INFO – performTask executed in 1002 ms
5. Advanced Example: Custom Annotation for Role-Based Authorization
We create a custom annotation @RoleCheck to restrict access based on user roles.
5.1 Define the Custom Annotation
package com.example.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleCheck {
String value(); // Role required to access the method
}
5.2 Implement the Annotation Processing
package com.example.aspects;
import com.example.annotations.RoleCheck;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RoleCheckAspect {
@Around("@annotation(roleCheck)")
public Object checkRole(ProceedingJoinPoint joinPoint, RoleCheck roleCheck) throws Throwable {
String currentUserRole = "USER"; // Simulated current user role
if (!currentUserRole.equals(roleCheck.value())) {
throw new SecurityException("Access Denied! Required Role: " + roleCheck.value());
}
return joinPoint.proceed();
}
}
5.3 Applying the Annotation
package com.example.services;
import com.example.annotations.RoleCheck;
import org.springframework.stereotype.Service;
@Service
public class SecureService {
@RoleCheck("ADMIN")
public String adminOnlyOperation() {
return "Admin operation performed!";
}
}
5.4 Testing the Authorization Check
If a user with role “USER” accesses the method, they receive an error:
Access Denied! Required Role: ADMIN
6. Conclusion
Custom annotations in Spring Boot offer a powerful way to improve code reusability, readability, and maintainability. They enable developers to encapsulate cross-cutting concerns such as logging, security, and validation without cluttering business logic.