Annotations are a feature introduced in JDK 1.5 and are used to explain the code.

They can annotate packages, classes, interfaces, fields, method parameters, local variables, etc.

Its main usages are as follows :

  • Generate documentation and javadoc documentation through metadata identified in the code;
  • Compilation check, through the metadata identified in the code, the compiler can check and verify during compilation;
  • Dynamic processing at compile time through metadata identified in the code, such as dynamically generating code;
  • Dynamic processing at runtime through metadata identified in the code, such as using reflection injection instances.

Below is the common categories of annotations :

  • Java’s standard annotations, including @Override (indicating overriding a method), @Deprecated (indicating a class or method is obsolete) and @SuppressWarnings (indicating warnings to be ignored);
  • Meta-annotations, they are annotations used to define annotations, including @Retention (indicating the stage at which the annotation is retained), @Target (indicating the scope of the annotation), @Inherited (indicating that annotations can be inherited) and @Documented (indicate whether to generate javadoc documents);
  • Custom annotations, it is possible to define annotations according to our own needs, and use meta-annotations to annotate custom annotations.

What You Need

  • About 10 minutes
  • A favorite text editor or IDE
  • Java 8 or later

1. Java Standard Annotations

1.1 @Override

It indicates that the current method definition will override the method in the parent class.

It can be useful for two reasons :

  • it extracts a warning from the compiler if the annotated method doesn’t actually override anything;
  • it can improve the readability of the source code.
public class AnnotationTest1 {
    public static void main(String[] args) {
        Person tom = new Student(1, "tom", 18);

        tom.greeting();
    }

    private static class Person {
        protected String name;
        protected int age;

        Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public void greeting() {
            System.out.println("hello my name is " + this.name + " and i am " + this.age + " years old");
        }
    }

    private static class Student extends Person {
        private int id;

        Student(int id, String name, int age) {
            super(name, age);
            this.id = id;
        }

        @Override
        public void greeting() {
            super.greeting();
            System.out.println("my id is " + this.id);
        }
    }
}

The output of above code snippet is below :

hello my name is tom and i am 18 years old
my id is 1

1.2 @Deprecated

It Indicates that the code is deprecated and should no longer be used, as it could be removed in future versions of the software.

If the code annotated with @Deprecated is used, the compiler will issue a warning.

In this way, it helps ensure that developers use the most up-to-date and efficient methods, contributing to cleaner, more maintainable code.

It also provides a mechanism for gracefully retiring old code without breaking existing implementations.

public class AnnotationTest2 {
    public static void main(String[] args) {
        Person jerry = new Person("jerry", 16);
        jerry.greeting();
    }

    private static class Person {
        protected String name;
        protected int age;

        Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        @Deprecated
        public void greeting() {
            System.out.println("hello my name is " + this.name + " and i am " + this.age + " years old");
        }
    }
}

1.3 @SuppressWarnings

It is used to instruct the compiler to suppress specific warnings for the annotated part of the code.

This feature is beneficial when the programmer is sure that the flagged code doesn’t pose a problem and the warning is unwarranted.

Suppressing such warnings can make the compiler output more readable by removing unnecessary noise.

Here are a few commonly suppressed warnings :

  • unchecked – This warning is generated when working with raw types in generics. It’s one of the most commonly suppressed warnings;
  • deprecation – This warning is generated when a deprecated class or method is used;
  • serial – This warning occurs when a serializable class does not declare a static final serialVersionUID field of type long;
  • rawtypes – This warning is generated when using raw types instead of parameterized types in generics;
  • unused – This warning is generated when a local variable, private method, or private field is declared but never used.
import java.util.ArrayList;
import java.util.List;

public class AnnotationTest3 {
    @SuppressWarnings("unchecked")
    public static void main(String[] args) {
        List myList = new ArrayList();
        myList.add("test");
    }
}

2. Meta Annotations

2.1 @Target

It describes the scope of defined annotations.

It takes an array of values from the java.lang.annotation.ElementType enum as its parameter.

The ElementType enum defines the possible kinds of elements that can be annotated in Java.

The values are :

  1. ANNOTATION_TYPE : An annotation type declaration;
  2. CONSTRUCTOR : A constructor declaration;
  3. FIELD : A field declaration;
  4. LOCAL_VARIABLE : A local variable declaration;
  5. METHOD : A method declaration;
  6. PACKAGE : A package declaration;
  7. PARAMETER : A parameter declaration;
  8. TYPE : A class, interface, enum, or record declaration;
  9. TYPE_PARAMETER : A type parameter declaration;
  10. TYPE_USE : A use of a type.

2.2 @Retention

It describes the time range for which annotations are retained.

It comes with some retention policies and these retention policies determine at which point an annotation is discarded.

There are three types of retention policies :

  • RetentionPolicy.SOURCE : The annotations annotated using the SOURCE retention policy are discarded at runtime;
  • RetentionPolicy.CLASS : The annotations annotated using the CLASS retention policy are recorded in the .class file but are discarded during runtime. CLASS is the default retention policy in Java;
  • RetentionPolicy.RUNTIME: The annotations annotated using the RUNTIME retention policy are retained during runtime and can be accessed in the program during runtime.
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

public class AnnotationTest4 {
    @Retention(RetentionPolicy.SOURCE)
    @interface SourceRetention {
        String value() default "Source Retention";
    }

    @Retention(RetentionPolicy.CLASS)
    @interface ClassRetention {
        String value() default "Class Retention";
    }

    @Retention(RetentionPolicy.RUNTIME)
    @interface RuntimeRetention {
        String value() default "Runtime Retention";
    }

    @SourceRetention
    private static class A {

    }

    @ClassRetention
    private static class B {

    }

    @RuntimeRetention
    private static class C {

    }

    public static void main(String[] args) {
        Annotation a[] = new A().getClass().getAnnotations();
        Annotation b[] = new B().getClass().getAnnotations();
        Annotation c[] = new C().getClass().getAnnotations();

        System.out.println(
                "Number of annotations attached to "
                        + "class A at Runtime: " + a.length);

        System.out.println(
                "Number of annotations attached to "
                        + "class B at Runtime: " + b.length);

        System.out.println(
                "Number of annotations attached to "
                        + "class C at Runtime: " + c.length);
    }
}

The output of above code snippet is below :

Number of annotations attached to class A at Runtime: 0
Number of annotations attached to class B at Runtime: 0
Number of annotations attached to class C at Runtime: 1

2.3 @Documented

By default, Java annotations are not shown in the documentation created by using the Javadoc tool.

public class AnnotationTest5 {

    @interface MyAnnotation {
        String value();
    }

    @MyAnnotation("i am a custom annotation")
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

javadoc AnnotationTest5.java

To ensure that our custom annotations are shown in the documentation, @Documented annotation can annotate our custom annotations.

import java.lang.annotation.Documented;

public class AnnotationTest6 {
    @Documented
    @interface MyAnnotation {
        String value();
    }

    @MyAnnotation("i am a custom annotation")
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

javadoc AnnotationTest6.java

2.4 @Inherited

Java, by default, does not allow the custom annotations to be inherited.

@inherited is a type of meta-annotation used to annotate custom annotations so that the subclass can inherit those custom annotations.

import java.lang.annotation.*;
import java.lang.reflect.AnnotatedElement;

@Inherited
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@interface CustomAnnotation {
    String value() default "custom annotation";
}

@CustomAnnotation(value = "i am your father")
class Father {
}

class Child extends Father {

}

public class AnnotationTest7 extends Father {
    public static void main(String[] arg) throws Exception {
        printAnnotationState(Father.class);
        System.out.println();
        printAnnotationState(Child.class);
    }

    static void printAnnotationState(AnnotatedElement ann) {
        System.out.println(ann);

        Annotation[] annotationsArray = ann.getAnnotations();

        for (Annotation annotation : annotationsArray) {
            System.out.println(
                    "Name : "
                            + annotation.annotationType());
            System.out.println(
                    "Value : "
                            + ((CustomAnnotation) annotation).value());
        }
    }
}

Below is the output of above code snippet :

class Father
Name : interface CustomAnnotation
Value : i am your father

class Child
Name : interface CustomAnnotation
Value : i am your father

3. Custom Annotations

After understanding the meta-annotations, we can create custom annotations and use the reflection interface to obtain those annotations in run time.

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;

public class AnnotationTest8 {
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyMethodAnnotation {
        public String title() default "";

        public String description() default "";
    }

    private static class TestMethodAnnotation {
        @MyMethodAnnotation(title = "test method", description = "this is for testing MyMethodAnnotation")
        public static void test() {

        }
    }

    public static void main(String[] args) {
        try {
            Method[] methods = TestMethodAnnotation.class.getDeclaredMethods();

            for (Method method : methods) {
                for (Annotation anno : method.getDeclaredAnnotations()) {
                    if (method.isAnnotationPresent(MyMethodAnnotation.class)) {
                        System.out.println("Annotation in Method '"
                                + method + "' : " + anno);

                        MyMethodAnnotation methodAnno = method
                                .getAnnotation(MyMethodAnnotation.class);

                        System.out.println("title = " + methodAnno.title());
                        System.out.println("description = " + methodAnno.description());
                    }

                    System.out.println();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

The output of above code snippet is below :

Annotation in Method 'public static void AnnotationTest8$TestMethodAnnotation.test()' : @AnnotationTest8.MyMethodAnnotation(title="test method", description="this is for testing MyMethodAnnotation")
title = test method
description = this is for testing MyMethodAnnotation

4. Use annotations in real world project

4.1 Spring Framework

The Spring framework has witnessed significant evolution since its inception.

One notable change has been the move from XML-based configurations to annotation-driven configurations.

XML-Based Configuration :

<bean id="myService" class="com.example.MyServiceImpl">

<property name="dependency" ref="myDependency"/>

</bean>

<bean id="myDependency" class="com.example.MyDependency"/> 

Annotation-Driven Configuration :

@Component
public class MyService { ... }

@Service
public class MyServiceImpl implements MyService {

@Autowired
private MyDependency myDependency;

}

The introduction of annotation-based configuration raised the question of whether this approach is better than XML.

The short answer is : it depends.

The long answer is that each approach has its pros and cons, usually, it is up to the developer to decide which strategy suits them better.

  • Due to the way they are defined, annotations provide a lot of context in their declaration, leading to shorter and more concise configuration;
  • However, XML excels at wiring up components without touching their source code or recompiling them.

4.2 Spring AOP + Custom Annotation

The most common example is to use custom annotations and Spring AOP aspects to achieve log management.

package com.example2;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.Duration;
import java.time.Instant;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Bean
    public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {
            HelloWorld hw = (HelloWorld) ctx.getBean("com.example2.Application$HelloWorld");
            hw.greeting();
        };
    }

    @Component
    class HelloWorld {
        @LogExecutionTime(additionalMessage = "To say helloworld after 1 second")
        public void greeting() throws InterruptedException {
            Thread.sleep(1000);
            System.out.println("Hello World");
        }
    }

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

    @Component
    @Aspect
    class LogAspect {
        private final Logger log = LoggerFactory.getLogger(this.getClass());

        @Around("@annotation(com.example2.Application$LogExecutionTime)")
        public Object logExecutionTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
            String className = methodSignature.getDeclaringType().getSimpleName();
            String methodName = methodSignature.getMethod().getName();
            Instant startTime = Instant.now();
            Object result = proceedingJoinPoint.proceed();
            String additionalMessage = methodSignature.getMethod().getAnnotation(LogExecutionTime.class)
                    .additionalMessage();
            long elapsedTime = Duration.between(startTime, Instant.now()).toMillis();
            log.info("Class Name: {}, Method Name: {}, Additional Message: {}, Elapsed Time: {}ms",
                    className, methodName, additionalMessage, elapsedTime);
            return result;
        }
    }
}

The output of above code snippet is below :

Hello World

Class Name: HelloWorld, Method Name: greeting, Additional Message: To say helloworld after 1 second, Elapsed Time: 1001ms