Creating Custom Annotations in Spring Boot

Vijayasankar Balasubramanian
4 min readJan 31, 2025

--

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.

--

--

Vijayasankar Balasubramanian
Vijayasankar Balasubramanian

Written by Vijayasankar Balasubramanian

Java Solution Architect, Java Full Stack Engineer

No responses yet