Creating Custom Annotations in Spring

Creating Custom Annotations in Spring

In a previous article, we delved into transaction management in Spring and demonstrated how to create a custom annotation for handling transactions. In this article, we will take a broader look at annotations in Spring, understand their significance, and learn how they can be utilized to achieve various functionalities.

Introduction

Annotations are a form of metadata that provide data about the program that is not part of the program itself. They have no direct effect on the operation of the code they annotate. In the Spring Framework, annotations are used extensively to drive the configuration, behaviour and functionality of applications in a declarative manner. Some common categories of annotations in Spring are:

  • Dependency Injection: Annotations like @Autowired, @Component, and @Service are used for defining beans and injecting dependencies.

  • Aspect-Oriented Programming (AOP): Annotations like @Aspect, @Before, and @After are used to define aspects and advice.

  • Data Access/Transactions: Annotations like @Transactional and @EnableTransactionManagement are used for transaction management, which we explored in depth in the previous article.

  • Web Development: Annotations like @Controller, @RequestMapping, and @RestController are used in Spring MVC for web application development.

  • Configuration: Annotations like @Configuration, @Bean, and @PropertySource are used for configuration purposes.

Creating Custom Annotations

In the previous article on transaction management, we created a custom annotation named @UserTx. This custom annotation was used to abstract away the configuration of transactional attributes and encapsulate them in a reusable annotation. Let’s now understand how you can create custom annotations in general. This post will focus on creating a custom Spring annotation to measure method execution time.

Defining the Custom Annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogExecutionTime {
}

The @Retention meta-annotation determines how long annotations with the annotated type are to be retained. It takes a RetentionPolicy enum as a value:

  • RetentionPolicy.SOURCE: These annotations are only available in the source code and are discarded by the compiler. They will not be present in the compiled bytecode. An example use-case could be a custom annotation that is used by a linter or checker tool during development.

  • RetentionPolicy.CLASS: These annotations will be recorded in the class file by the compiler but need not be retained by the VM at runtime. This is the default retention policy if none is specified.

  • RetentionPolicy.RUNTIME: These annotations are recorded in the class file by the compiler and retained by the JVM at runtime, so they can be read reflectively.

The @Target meta-annotation indicates where an annotation type can be used. It takes an ElementType enum as a value:

  • ElementType.ANNOTATION_TYPE: Can be applied to an annotation type.

  • ElementType.CONSTRUCTOR: Can be applied to a constructor.

  • ElementType.FIELD: Can be applied to a field or property.

  • ElementType.LOCAL_VARIABLE: Can be applied to a local variable.

  • ElementType.METHOD: Can be applied to a method-level annotation.

  • ElementType.PACKAGE: Can be applied to a package declaration.

  • ElementType.PARAMETER: Can be applied to the parameters of a method.

  • ElementType.TYPE: Can be applied to any element of a class.

The Aspect Class

Aspects in Java, or more specifically in Aspect-Oriented Programming (AOP), are modules of cross-cutting concerns. They are a way of modularizing concerns that cut across the typical divisions of responsibility, such as logging and transaction management.

These cross-cutting concerns are functionalities that span multiple points in your application, making them different from the core business logic. Examples include security, logging, data validation, and transaction management.

To implement the behaviour that will be triggered when the annotated methods are invoked, we create an aspect:

@Aspect
@Component
@Slf4j
public class LogExecutionTimeAspect {
    @Around("@annotation(LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        Object proceed = joinPoint.proceed();

        long executionTime = System.currentTimeMillis() - start;

        log.info(joinPoint.getSignature() + " executed in " + executionTime + "ms");

        return proceed;
    }
}

Let's break down how this method works:

  • @Around("@annotation(Logged)"): The @Around annotation is used to apply advice before and after the method invocation. It is the most powerful form of advice. The parameter @annotation(LogExecutionTime) specifies the pointcut expression - it matches any execution of a method that is annotated with @LogExecutionTime.

  • ProceedingJoinPoint joinPoint: This is a special argument that we can pass to our advice methods. This argument provides us with some utilities, for example, we can use it to proceed with the original method call (joinPoint.proceed()).

  • long start = System.currentTimeMillis();: Here, we are noting the system time just before the method execution.

  • Object proceed = joinPoint.proceed();: With this line, we tell Spring to proceed with the method execution. The proceed() method also returns the result of the method execution, which we will return at the end of our advice.

  • long executionTime = System.currentTimeMillis() - start;: After the method execution, we note the system time again and subtract the start time from it to get the execution time.

  • log.info(joinPoint.getSignature() + " executed in " + executionTime + "ms");: This line logs the signature (name) of the method and how long it took to execute.

  • return proceed;: At the end, we return the result of the method execution.

    Also, we use yet another annotation @Slf4j from our good old friend Lombok package to provide a logger.

Usage Example

With our custom annotation ready, let's see it in action. Here's how you could create a doSomething() method in MyService class with logic that lasts a random number of seconds:

@Service
public class MyService {
    @LogExecutionTime
    public void doSomething() {
        Random random = new Random();
        int delay = random.nextInt(10);  // random delay between 0-9 seconds
        try {
            Thread.sleep(delay * 1000);  // sleep for the random number of seconds
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new RuntimeException(e);
        }
    }
}

In this example, doSomething method will pause its execution for a random number of seconds between 0 and 9 each time it's called. This will give us varied execution times when we use the @LogExecutionTime annotation to measure its execution time.

Here is the example log returned by our brand-new annotation:
2023-05-29 14:22:32.033 INFO 9260 --- [nio-8080-exec-1] c.s.annotation.LogExecutionTimeAspect : void com.samulewski.annotation.MyService.doSomething() executed in 6003ms

PostProcessors and Stereotype Annotations

It's also important to note that annotations such as @Repository, @Configuration, and other stereotype annotations in Spring are handled in a slightly different manner. They are processed by BeanPostProcessors and play a crucial role in guiding the Spring container to apply specific behaviour during the bean lifecycle.

Each of these stereotype annotations has its own post-processor (for instance, @Configuration classes are handled by ConfigurationClassPostProcessor), and they provide a powerful mechanism to modify bean definitions, bean instances, and perform other tasks before and after the Spring container instantiates and configures your beans.

If you're curious to dive deeper into the specifics of these annotations, how PostProcessors work, or wish to enhance your understanding of advanced Spring concepts, do let me know. We can explore these topics in greater detail in future posts.

Conclusion

Annotations are powerful tools in Java and Spring, and they can be used to build clean and maintainable applications. With custom annotations, we can define reusable modules of behaviour that can be easily applied across our codebase. We saw how to create a simple custom annotation to log method execution time. However, this is just scratching the surface of what can be achieved with custom annotations and aspects. They can be used for transaction management, security, caching, and much more. Happy coding!

Did you find this article valuable?

Support Jarosław Samulewski by becoming a sponsor. Any amount is appreciated!