Java Annotations

In this article, we are going to discuss Annotations, Annotation is a very useful feature in Java, many Java frameworks like spring-boot, Hibernate (ORM Framework), Junit (Testing Framework), and libraries like Lombok depend heavily on annotation. In this article, we will cover the basics of annotation, see coding examples, and learn about custom annotations.

Content

Before jumping to annotations, let's first understand about metadata.
Metadata, it's data that provides information about various elements of Java code like classes, methods, fields, parameters, and packages. This metadata is used by the compiler, runtime environment, and framework. Some Examples are Annotation and Javadoc comments.

Now coming to Annotation, It is just metadata, that provides information about application code but not part of it. We can attach annotations to various Java code elements like methods, classes, fields, parameters, and packages. It is used by compilers, frameworks, and runtime environments to perform logic like detecting errors, suppressing warnings, generating code, and configuration files at runtime.
For Example,
we have annotation @Override, we use it on top of the method in the subclass to tell that this method is from the superclass and we are overriding it in the subclass. Here we just put annotation on top of the method, and that annotation also doesn't have any logic inside it.
Then who performs this validation that this method is from the superclass? This logic is performed by compiler, development tools like IDE. Let's say you put @Override on top of a method that is not part of the super class then it will give you a compile-time error, again all this logic is performed by the compiler.

In Java standard library various annotations are defined in Java. lang and java.lang.annotatioan packages, below are some of them.

  • @Override, it provides information to compile about the superclass method overriding in the subclass.
    • In case the method in the subclass is annotated with @Override and that method is not part of the superclass it will give compile time error, Like "error: method does not override or implement a method from a supertype @Override"

  • @Deprecated, this annotation is used to mark any Java code elements (class, fields, methods, etc) as deprecated and no longer valid for use.

  • @SuppressWarnings, this we mostly use to suppress compiler warnings like deprecation, rawtypes, and unchecked on Java code elements (class, fields, methods, etc).
    • deprecation, this warning we get when we use any method or fields that are marked as deprecated using @Deprecated annotation but we still want to use them.
    • unchecked, it happens when the compiler can not ensure type safety. Let's say we are casting an Object to a String without checking its type. another example is let's say you are adding a String type value into a list of rawtype.
    • rawtypes, it happens when we create an object of type parametrized interface or class, without specifying its type. example if we define a list like List list = new ArrayList();

  • @SafeVarargs, this is mostly used on top of the method and constructor, it tells the compiler that this method/constructor is safe to use with varargs parameter of generic type.

  • @FunctionalInterface, this is introduced in Java 8, It is annotated on top of the interface and it helps to mark that interface as a functional interface, whose implementation is provided by Lambda expression. Also, functional interface means SAM (Single Abstract Method), so if any interface annotated with @FunctionalInterface does not follow the rule of functional interface then this annotation will give a compile time error.

Java libraries like Lombok and frameworks like Spring Boot depend heavily on annotations.

  • In Lombok, it reduces boilerplate code by providing annotation that can generate code at compile time. e.g. @Getter, @Setter, generate getter and setter fields for the class annotated with these annotations. @EqualsAndHashCode, generate equals and hashcode method for the class.
  • In Spring Boot, we have a huge list of annotations for various purposes. Some of them are
    • @SpringbootApplication: This we mostly use to mark the class as the main class or starting point of the spring application. It combines 3 annotations within itself, @Configuration, @EnableAutoConfiguration and @ComponentScan.
    • @Autowired, Spring use it for injecting dependency, @RESTController @RequestMapping for creating Restful webservices.
  • ORM Framework like Hibernate, also uses annotation to map Java classes to tables. Examples, @Id, @Entity, @Table, @Column.
  • Testing framework like Junit also uses annotation to define test cases, setup methods, and test-related configurations. E.g , @Test, @Before, @After.


So far we read about annotations provided by Java, but can we create our annotations? Ans is yes, we can create our annotation. Let's see the code first and then we will understand it in detail,


  • To create annotation we must use the '@interface' keyword, it's similar as we use the 'class' keyword to create a class and the 'interface' keyword to create an interface.
  • On top of it we must use two annotations, @Retention and @Target.


Annotations that apply to another annotation are called meta-annotations. So here @interface, @Retention, and @Target all are meta-annotations.


  • @Retention, annotation tells how long annotation should retain, that is it indicates if it's available at runtime or compile time.
    • It accepts one parameter of type RetentionPolicy Enum, it has 3 values.
      • RetentionPolicy.RUNTIME, for custom annotation we mostly used this retention policy, it allows the annotation to be available at runtime, and using Java reflection API we (The programmer) can access it. Examples: @Deprecated
      • RetentionPolicy.CLASS, allows the annotation to be available in compiled code (.class file), so such annotation is available during compilation and in generated .class files but cannot access at runtime even through Java reflection API.
      • RetentionPolicy.SOURCE, allows the annotation to be available in source code (.java file) but it is not included in compiled code (.class file). Such annotation can be accessed during compile time by the compiler but its presence/absence will not impact application operations. It is mostly used by IDEs (to show warning and error while writing code) and compilers. It cannot be accessed through Java Reflection API. Example: @Override, @SupressWarnings.
  • @Target, defines the target of our custom annotation, and on what program elements we can apply it, like class, methods, fields, and parameters. It accepts one/list of the parameters of the type ElementType enum, ElementType has various elements that are as below,
    • ElementType.TYPE: Indicates that the annotation can be applied to a class, interface, or enumeration.
    • ElementType.METHOD: Indicates that the annotation can be applied to a method.
    • ElementType.FIELD: Indicates that the annotation can be applied to a field.
    • ElementType.PARAMETER: Indicates that the annotation can be applied to a method parameter/argument.
    • ElementType.CONSTRUCTOR: Indicates that the annotation can be applied to a class constructor.
    • ElementType.LOCAL_VARIABLE: Indicates that the annotation can be applied to a local variable.
    • ElementType.ANNOTATION_TYPE: Indicates that the annotation can be applied to another annotation.
    • ElementType.PACKAGE: Indicates that the annotation can be applied to a package.

  • In our example, inside annotation we have some elements like String value and int count. Elements in custom annotation hold the data provided by the user within annotation, we can define annotation elements as a method without a body, and it can also have default values as we define below examples.
    • Our custom annotation can have no, one, or more than one element, we can define it as mentioned below.
      • If there is no element in the annotation then it is called a marker annotation.
      • If it accepts only one then it is called a single value annotation.
      • If it accepts more than one then it is called a full annotation.

So, far we understood what is Annotation, read about some Java standard annotations and how can we create our custom annotation. but annotation is just metadata, the actual logic of annotation performed by the compiler, let's understand annotation processing and how the compiler handles it.
The compiler uses an annotation processor, Which processes annotation at compile or run time, and if required generates source code or resources.

  • In the compilation process Java files (.java) convert into Class files (.class).
  • During this compilation, the compiler also scans for annotation and once an annotation is found, it processes that annotation using an annotation processor.
    • What is an annotation processor? An annotation processor is a class that implements javax.annotation.processing.Processor interface. such class having actual logic that will perform on behalf of annotation. This may include logic to generate source code and new resources.
    • To create our Custome annotation processor we can implement javax.annotation.processing.Processor interface and provide an implementation of all of its methods or can extend javax.annotation.processing.AbstractProcessor abstract class and override its 'process' method.
  • Compiler finds and loads annotation processor using Service Provider Interface (SPI) mechanism. Processor classes are specified in a file named javax.annotation.processing.Processor is located in the META-INF/services directory of processor JAR files. During compilation, the compiler locates and instantiates these processor classes to perform annotation processing.
    • here SPI, is a design pattern, using which we can customize applications or libraries by providing implementation of certain interfaces. It enables dynamic discovery and loading of the service provider at run time without explicit configuration. using this, whatever interface we want to implement we have to create a file of the fully qualified name of that interface inside the META-INF/services directory, inside the file we need to write the fully qualified name of the class which will provide implementation of it.
      • here we implemented javax.annotation.processing.Processor interface, so we created a file with that name inside the META-INF/services directory, and inside this file, we mentioned the full qualified name of our custom annotation processor class.
    • To create a file in the META-INF/services directory, we can do all this configuration manually or we can use the below dependency and use annotation '@`AutoService(Processor.class)' on top of our custom annotation processor, which will handle all this configuration automatically.

            <groupId>com.google.auto.service</groupId>
            <artifactId>auto-service</artifactId>

  • Compiler perform processing in rounds, so it first process source code, let's say it finds some annotation and after its processing, some new source code is generated, then in the next round processing only the source files that have changed since the previous compilation or newly introduced by the annotation processor.
  • In the end compiled bytecode files (".class") generate.

In case we are just creating small programs we can use the command javac to compile code and we can pass our custom processor in this command, so that compiler can use it.

javac -processor com.coderstuff01.MyAnnotationProcessor MyClass.java

Thank you for reading this article, hope you learn something new and find it interesting. if you have anything to share with us please feel free to comment below.

Comments