Custom annotations in Java

Java developers are not limited to using built-in annotations only but can also create their own annotations to provide additional functionality. For example many Java frameworks define custom annotations to provide (or at least simplify usage of) such functionality as:

  • unit testing
  • ORM mapping
  • bean validation
  • Java class to XML mapping
  • web-service description

Of course, the list is not even one percent complete. In this article I would like to describe how to create custom annotations and later access them through reflection.

Creating custom annotation

Annotation type definition is very similar to an interface definition:

public @interface Version {
    int major();
    int minor() default 0;
    String date();
}

The most visible difference is the usage of interface keyword preceded by the at sign (@) when defining annotation type. The similar syntax is not a coincidence because in fact annotations are visible to the virtual machine as plain interfaces extending Annotation interface and the annotation elements are visible as abstract methods. It is also possible to create static fields, static classes and enums inside an annotation. It is, however, impossible to create a new annotation type by extending (inheriting from) existing annotation type.

Another important difference is the ability to specify a default value for the annotation element. If the element has a default value, it can but don’t have to be specified when using the annotation. If it is not specified, the default value is used. The default value must be a constant and can never be a null value. The latter requirement is somewhat inconvenient and forces programmers to use other default values like “” or Void.class.

Additionally, annotation elements cannot have arguments, cannot define thrown exceptions, cannot be generic and their element types are limited to:

  • primitive types like int, long, double or boolean
  • String class
  • Class class with optional bounds
  • enum types
  • annotation types
  • an array containing one of the above types

Here is another annotation using most of the types above:

public @interface ClassInfo {
    enum AccessLevel { PUBLIC, PROTECTED, PACKAGE_PROTECTED, PRIVATE};

    String author();
    Version version();
    AccessLevel accessLevel() default AccessLevel.PACKAGE_PROTECTED;
    String[] reviewers() default { };
    Class<?>[] testCases() default { };
}

Please, note that in the second element we refer to the previously defined Version annotation. In the next one we use enum type (defined within the same annotation) as the element type and we also provide one of its values as a default value. The last two elements can be assigned an array – if they are not set, they default to an empty array.

Meta-annotations

Java provides several meta-annotations – annotations which can be applied to other annotations. The custom annotation can be annotated with one or more of such meta-annotations to provide additional information how the custom annotation can be used.

@Target

@Target annotation restricts to which source code elements the custom annotation can be applied. The value of the @Target annotation is an array containing one or more of the following values:

  • ElementType.ANNOTATION_TYPE – can be applied to another annotation type (creates meta-annotation)
  • ElementType.CONSTRUCTOR – can be applied to a constructor
  • ElementType.FIELD – can be applied to a field (includes enum constants)
  • ElementType.LOCAL_VARIABLE – can be applied to a local variable
  • ElementType.METHOD – can be applied to a method
  • ElementType.PACKAGE – can be applied to a package (placed in package-info.java file)
  • ElementType.PARAMETER – can be applied to a method parameter
  • ElementType.TYPE – can be applied to a type (class, interface, enum or annotation)
  • ElementType.TYPE_PARAMETER – can be applied to a type parameter (new concept in Java 8)
  • ElementType.TYPE_USE – can be applied to a use of type (new concept in Java 8)

If the @Target annotation is missing, the annotation can be applied almost everywhere except type parameter.

@Retention

@Retention annotation indicates how the custom annotation is stored. Its value can be one of the following values:

  • RetentionPolicy.SOURCE – annotations are analyzed by the compiler only and are never stored into class files
  • RetentionPolicy.CLASS – annotations are stored into class files but are not retained by the virtual machine at run-time
  • RetentionPolicy.RUNTIME – annotations are stored into class files and are retained by the virtual machine at run-time so they are available via reflection

If the @Retention annotation is missing, the value defaults to Retention.CLASS. In most cases RetentionPolicy.RUNTIME policy is used in order to be able to examine the annotations at run-time.

@Documented

@Documented annotation indicates whether the custom annotation should appear on the annotated elements in Javadoc documentation. If @Documented is applied to the custom annotation, all classes annotated with the custom annotation will be marked as such in Javadoc documentation. If @Documentation is missing, Javadoc documentation may contain information about the custom annotation (depending on its access modifiers and JavaDoc parameters) but won’t contain information about which classes were annotated with the custom annotation.

@Inherited

@Inherited annotation indicates whether the custom annotation is inherited from the super class. This annotation does not have any effect if the custom annotation is applied to anything other than a class. By default the annotations are not inherited.

@Repeatable

@Repeatable annotation indicates whether the custom annotation can be applied to the same source code element multiple times. By default the same annotation type can be used only once on the same source code element.

Accessing annotations via reflection

Information about annotations applied to classes, methods and many other elements can be extracted using AnnotatedElement interface which is implemented by the following reflective classes: Class, Constructor, Field, Method, Package and Parameter. The presence of the annotation can be checked using isAnnotationPresent() method and the actual annotations can be retrieved using methods: getAnnotation(), getAnnotations() and few more.

The element values can be accessed by calling appropriate methods (named the same as annotation elements) on the returned instances of Annotation interface.

If the annotations have a retention policy different than RetentionPolicy.RUNTIME, they won’t be accessible though reflection.

Example

As an example we will create a very, very simple annotation-based unit test framework. The methods to test will be annotated using following annotation:

package com.example.customannotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest {
    String name() default "";
    MyTestState state() default MyTestState.ACTIVE;
    Class<? extends Throwable> expected() default None.class;
    
    static class None extends Throwable {
    }
}

Annotation MyTest uses @Retention(RetentionPolicy.RUNTIME) to make it accessible through reflection at run-time and @Target(ElementType.METHOD) to restrict its usage only to methods. Because we cannot use null as a default value for expected element, we create an empty class None and set its class object as a default value. Additionally, we allow the tests to be enabled or disabled using this enumeration:

package com.example.customannotation;

public enum MyTestState {
    ACTIVE, INACTIVE
}

Once we have the annotation ready, we can apply it to the test methods:

package com.example.customannotation;

import static com.example.customannotation.MyAsserts.*;

public class SimpleTestCase {

    @MyTest(name = "test1WithCustomName", state = MyTestState.ACTIVE)
    public void test1() {
        assertEquals(2, 1 + 1);
        assertEquals(Integer.parseInt("-3"), -3);
    }
    
    @MyTest(expected = NumberFormatException.class)
    public void test2() {
        Integer.parseInt("1.23ddd");
    }
    
    @MyTest(state = MyTestState.INACTIVE)
    public void test3() {
        throw new IllegalStateException("Test case is inactive");
    }
}

The last step is to create a simple test runner which accepts a list of classes to test:

package com.example.customannotation;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MyTestRunner {

    public void run(Class<?>... klasses) {
        for (Class<?> testClass : klasses) {
            runTestClass(testClass);
        }
    }
    
    private void runTestClass(Class<?> klass) {
        for (Method method : klass.getMethods()) {
            MyTest annotation = method.getAnnotation(MyTest.class);
            if (annotation != null)
                runTestMethod(klass, method, annotation);
        }
    }

    private void runTestMethod(Class<?> klass, Method method, MyTest annotation) {
        if (annotation.state() != MyTestState.ACTIVE)
            return;
        try {
            System.out.println("Running test: " + getTestName(method, annotation));
            Object testInstance = klass.newInstance();
            method.invoke(testInstance);
            System.out.println("SUCCESS");
        } catch (InstantiationException e) {
            System.err.println("FAILED: Failed to instantiate class " + klass.getName());
        } catch (IllegalAccessException e) {
            System.err.println("FAILED: Failed to call test method " + method.getName());
        } catch (InvocationTargetException e) {
            checkThrowable(annotation, e.getCause());
        }
    }

    private static String getTestName(Method method, MyTest annotation) {
        return !annotation.name().isEmpty() ? annotation.name() : method.getName();
    }
    
    private void checkThrowable(MyTest annotation, Throwable th) {
        if (annotation.expected() == th.getClass())
            System.out.println("SUCCESS");
        else
            System.out.println("FAILED: " + th.getMessage());
    }
}

and use the runner to execute the tests:

package com.example.customannotation;

import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
        MyTestRunner runner = new MyTestRunner();
        runner.run(SimpleTestCase.class);
    }

}

The Method.getAnnotation() method is used to extract MyTest annotation (if present) for each method. Later the elements of MyTest annotation are accessed in getTestName() and checkThrowable() methods. The lack of null checks in both methods is normal because annotation elements cannot be null.

Conclusion

Creating custom annotations is not a very common task because most of the time we are just using existing annotations defined in various frameworks. However, sometimes it may be necessary to create our own annotation to extend existing framework (e.g. Bean Validation or Spring). To keep the article concise I have almost silently omitted several rarely used concepts like repeated annotations or use of types. I am going to cover them in near future.

The complete source code for the example is available at GitHub.

Another example of custom annotations is described in article Custom bean validation constraints.

Advertisement

About Robert Piasecki

Husband and father, Java software developer, Linux and open-source fan.
This entry was posted in Java and tagged , . Bookmark the permalink.

3 Responses to Custom annotations in Java

  1. Wow, one of the best article on relatively lesser known subject. I was really looking forward to create my own annotation e..g @stateless thanks dude.

  2. nighteblis says:

    greate sharing of the creating and accessing annotation , thanks a lot.

  3. Shivang says:

    Nice introduction to custom annotations. Would make one suggestion though…in the section for @Repeatable annotation, I think it would be nice to mention it’s only available since Java 8…I was looking for that annotation in Java 7 code! 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.