Creating and using annotations in Java

Java annotations are simple data that we can attach to program elements like classes, fields or methods. True power of annotations lies in the fact that we are able to retrieve this information at runtime. To demonstrate that we will create simple annotation called @RunAtStartup. This annotation will allow programmers to register classes that should be instantiated at program startup, programmers will be able to specify class priority (via priority named element) and optionally specify name of method that should be run on class instance (via method optional element).

Here is example usage of @RunAtStartup annotation:

// StartupClass1.java:
package mc.annotations;
import mc.annotations.RunAtStartup;

@RunAtStartup(priority = 10)
public class StartupClass1 {
    public void run() {
        System.out.println("Class 1 initialized!");
    }
}

// StartupClass2.java:
package mc.annotations;
import mc.annotations.RunAtStartup;

@RunAtStartup(priority = 100, method = "initialize")
public class StartupClass2 {
    public void run() {
        throw new IllegalStateException("This method should not be called");
    }

    public void initialize() {
        System.out.println("Class 2 initialized!");
    }
}

annotations are declared using @interface keyword, here is declaration of our @RunAtStartup annotation:

package mc.annotations;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RunAtStartup {
    int priority();
    String method() default "run";
}

annotation declaration is very similar to interface declaration, in fact annotations are interfaces that are implemented by JVM behind the scene.

annotations can have zero or more elements (like priority or method), you can provide default value for element via default keyword. If you provide default value users won’t be required to specify element value when applying annotation. Notice that we are providing default value for method element, thus we don’t need to provide it in StartupClass1 file, but still we are able to override default value as demonstrated in StartupClass2 file. We always must provide value for elements that doesn’t have default value.

Elements of annotation must have one of types:

  • boolean, byte, short, int, long (wrapper classes like Integer cannot be used)
  • String type
  • enum type
  • class type
  • another annotation type
  • array of any of the above types

For example:

public @interface TestAnnotation {
    Class<Comparable<?>> comparator();
    boolean ascendingSort();
    String sortName();
    String[] propertiesToSortBy();
}

There are two important standard annotations that help us create new annotations, they are @Target and @Retention. @Target tells Java compiler on what program elements annotation can be used, e.g. our @RunAtStartup annotation have @Target(ElementType.Type) target so it can be only used on classes and interfaces, if we try to use it on field we get an error:

@RunAtStartup(priority = 20)
private static int foo = 0;

// Error: java: annotation type not applicable to this kind of declaration

@Retention tells compiler and JVM if annotation should be accessible at runtime. Some annotations exists only in Java source code, others exists in .class files but cannot be accessed by reflection API, finally there are annotations with retention RetentionPolicy.RUNTIME that can be accessed using reflection (our @RunAtStartup annotation is of that kind).

Finally there is a shortcut for creating annotations that have only single element, if we name that element value we will be able to omit it name e.g.

// SimpleAnnotation.java:
package mc.adnotations;

public @interface SimpleAnnotation {
    String value() default "ok";
}

// Usage:
@SimpleAnnotation("foo")
// instead of @SimpleAnnotation(value = "foo")
public class Main { ... }

This also works with arrays:

// SimpleAnnotation.java:
package mc.adnotations;

public @interface SimpleAnnotation {
    String[] value() default { "ok" };
}

// Usage:
@SimpleAnnotation({ "foo", "bar" })
// instead of @SimpleAnnotation(value = { "foo", "bar" })
public class Main { ... }

TIP: For single element arrays we can write @SimpleAnnotation("foo") instead of @SimpleAnnotation({ "foo" }).

Finally it’s time to implement @RunAtStartup behaviour. Unfortunately there is no simple way to get list of all classes in a package in Java. To keep our example simple we assume that our program is loaded from directory (not from jar archive), in that case list of classes in a package is simply a list of .class files in package directory. To get class files we use Java 7 new file API:

List<Class<?>> getAllClassesInPackageContaining(Class<?> clazz) 
    throws IOException 
{
    String clazzPackageName = clazz
            .getPackage()
            .getName();

    String clazzPath = clazz
            .getResource(".")
            .getPath();

    Path packagePath = Paths.get(clazzPath)
            .getParent();

    final List<Class<?>> packageClasses = new ArrayList<>();

    Files.walkFileTree(packagePath, new SimpleFileVisitor<Path>() {
        @Override
        public FileVisitResult visitFile(
                Path file, BasicFileAttributes attrs) 
                throws IOException 
        {
            String filename = 
                file.getName(file.getNameCount()-1).toString();

            if (filename.endsWith(".class")) {
                String className = filename.replace(".class", "");

                try {
                    Class<?> loadedClazz = Class.forName(
                        clazzPackageName + "." + className);
                        
                    packageClasses.add(loadedClazz);
                }
                catch(ClassNotFoundException e) {
                    System.err.println(
                        "class not found: " + e.getMessage());
                }
            }

            return super.visitFile(file, attrs);
        }
    });

    return packageClasses;
}

And our main program contained in Main class looks like this:

private static class RunAtStartupData {
    Object object;
    Method method;
    int priority;

    public RunAtStartupData(
        Object object, Method method, int priority) 
    {
        this.object = object;
        this.method = method;
        this.priority = priority;
    }

    public void callMethod() throws Exception {
        method.invoke(object);
    }
}

public static void main(String[] args) throws Exception {
    List<Class<?>> packageClasses =
        getAllClassesInPackageContaining(Main.class);

    List<RunAtStartupData> registrations = new ArrayList<>();

    for (Class<?> clazz : packageClasses) {
        RunAtStartup runAtStartup = 
            clazz.getAnnotation(RunAtStartup.class);
        if (runAtStartup == null) continue;

        Object instance = clazz.newInstance();
        Method method = clazz.getMethod(runAtStartup.method());

        registrations.add(new RunAtStartupData(
            instance, method, runAtStartup.priority()));
    }

    Collections.sort(
        registrations,
        Comparator.<RunAtStartupData>comparingInt(x -> x.priority)
              .reversed());

    for (RunAtStartupData registration : registrations) {
        registration.callMethod();
    }
}

We use clazz.getAnnotation(RunAtStartup.class) to check if annotation was applied to given class. Another tricky part is use of Java 8 lambda and new Comparator API to sort all startup classes by their priorities (classes with higher priority should run first).