processing meta annotations with Java

Everybody who is using Spring these days knows that Spring has many annotations that are just used to group together a couple of other annotations and so to keep the number of annotations small. For example the @RestController annotation combines basically the @Controller and the @ResponseBody annotation so that the user only needs to annotate his class with one annotation instead of two annotations.

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    String value() default "";
}

The JEE @Stereotype annotation works basically the same way.

In this post I show how to define and evaluate such meta annotations in plain Java without using Spring or JEE.

The annotations

I define the annotations for the example within one class I called WorkingDays. I define one annotation for each day of the week and two annotations that group working days and weekend days by referencing the other annotations:

public class WorkingDays {

    @Retention(RUNTIME)
    @interface OnMonday { }

    @Retention(RUNTIME)
    @interface OnTuesday { }

    @Retention(RUNTIME)
    @interface OnWednesday { }

    @Retention(RUNTIME)
    @interface OnThursday { }

    @Retention(RUNTIME)
    @interface OnFriday { }

    @Retention(RUNTIME)
    @interface OnSaturday { }

    @Retention(RUNTIME)
    @interface OnSunday { }

    @Retention(RUNTIME)
    @OnMonday @OnTuesday @OnWednesday @OnThursday @OnFriday
    @interface OnWorkday { }

    @Retention(RUNTIME)
    @OnSaturday @OnSunday
    @interface OnWeekend { }

    public static List<Class<? extends Annotation>> dayOfWeekAnnotations() {
        return Arrays.asList(WorkingDays.OnMonday.class, WorkingDays.OnTuesday.class, WorkingDays.OnWednesday.class,
                WorkingDays.OnThursday.class, WorkingDays.OnFriday.class, WorkingDays.OnSaturday.class,
                WorkingDays.OnSunday.class);
    }
}

In addition to that there is a method dayOfWeekAnnotations() which returns the annotations for the single days, but not the grouping ones.

The class using the annotations

I define a class Employee with three nested subclasses, one is using the @OnWorkday annotation, another one the @OnWeekend annotation and the third one both of them. Please notice, that the annotations for the separate days are not used here:

public class Employee {

    @OnWorkday
    public static class OnlyOnWorkdayEmployee extends Employee {
    }

    @OnWeekend
    public static class OnlyOnWeekendEmployee extends Employee {
    }

    @OnWorkday @OnWeekend
    public static class EveryDayEmployee extends Employee {
    }

    public String simpleClassName() {
        return getClass().getSimpleName();
    }
}

The class evaluating the annotations

To evaluate this, I create a class named Office in which I first create three different employees, each of a different type.

Then I iterate over all the annotations for the single days – remember, the employees use the meta annotations, not the ones for the days – and print out which of the employees works on which day. For this I use a utility class MetaAnnotationUtil which I will show after the output.

public class Office {
    private static final Logger LOGGER = LoggerFactory.getLogger(Office.class);

    public static void main(String[] args) {
        List<Employee> employees = getEmployees();

        WorkingDays.dayOfWeekAnnotations()
                .forEach(dayAnnotation -> {
                    LOGGER.info("\n{}", dayAnnotation.getSimpleName());
                    employees.forEach(employee ->
                            LOGGER.info(" {} is working: {}", employee.simpleClassName(),
                                    employeeWorksOn(employee, dayAnnotation)));
                });
    }

    private static boolean employeeWorksOn(Employee employee, Class<? extends Annotation> dayAnnotation) {
        return MetaAnnotationUtil.hasMetaAnnotation(employee.getClass(), dayAnnotation);
    }

    @NotNull
    private static List<Employee> getEmployees() {
        List<Employee> employees = new ArrayList<>();
        employees.add(new Employee.EveryDayEmployee());
        employees.add(new Employee.OnlyOnWorkdayEmployee());
        employees.add(new Employee.OnlyOnWeekendEmployee());
        return employees;
    }
}

This is the output when running the program:

13:04:18.302 [main] INFO  com.sothawo.annotations.Office - 
OnMonday
13:04:18.342 [main] INFO  com.sothawo.annotations.Office -  EveryDayEmployee is working: true
13:04:18.342 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWorkdayEmployee is working: true
13:04:18.343 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWeekendEmployee is working: false
13:04:18.343 [main] INFO  com.sothawo.annotations.Office - 
OnTuesday
13:04:18.343 [main] INFO  com.sothawo.annotations.Office -  EveryDayEmployee is working: true
13:04:18.343 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWorkdayEmployee is working: true
13:04:18.343 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWeekendEmployee is working: false
13:04:18.343 [main] INFO  com.sothawo.annotations.Office - 
OnWednesday
13:04:18.344 [main] INFO  com.sothawo.annotations.Office -  EveryDayEmployee is working: true
13:04:18.344 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWorkdayEmployee is working: true
13:04:18.344 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWeekendEmployee is working: false
13:04:18.344 [main] INFO  com.sothawo.annotations.Office - 
OnThursday
13:04:18.344 [main] INFO  com.sothawo.annotations.Office -  EveryDayEmployee is working: true
13:04:18.344 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWorkdayEmployee is working: true
13:04:18.345 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWeekendEmployee is working: false
13:04:18.345 [main] INFO  com.sothawo.annotations.Office - 
OnFriday
13:04:18.346 [main] INFO  com.sothawo.annotations.Office -  EveryDayEmployee is working: true
13:04:18.346 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWorkdayEmployee is working: true
13:04:18.346 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWeekendEmployee is working: false
13:04:18.347 [main] INFO  com.sothawo.annotations.Office - 
OnSaturday
13:04:18.347 [main] INFO  com.sothawo.annotations.Office -  EveryDayEmployee is working: true
13:04:18.347 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWorkdayEmployee is working: false
13:04:18.347 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWeekendEmployee is working: true
13:04:18.348 [main] INFO  com.sothawo.annotations.Office - 
OnSunday
13:04:18.349 [main] INFO  com.sothawo.annotations.Office -  EveryDayEmployee is working: true
13:04:18.349 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWorkdayEmployee is working: false
13:04:18.349 [main] INFO  com.sothawo.annotations.Office -  OnlyOnWeekendEmployee is working: true

The class doing the magic

In the MetaAnnotationUtil class I setup a Set to collect the annotations and then add the found annotations if I have them not in the set already and recursively process the annotations of the current annotation:

public abstract class MetaAnnotationUtil {

    /**
     * checks wether an annotated element has an annotaion directly or as a meta annotation (annotation on annotation)
     *
     * @param annotatedElement
     *         the annotated element
     * @param annotationClass
     *         the annotation class to search
     * @return true if the annotaion class is found
     */
    public static boolean hasMetaAnnotation(@NotNull final AnnotatedElement annotatedElement,
                                            @NotNull Class<? extends Annotation> annotationClass) {
        final Set<Class<? extends Annotation>> foundAnnotations = new HashSet<>();
        findAllAnnotations(annotatedElement, foundAnnotations);
        return foundAnnotations.contains(annotationClass);
    }

    /**
     * do a meta (recursive) search on the annotated element and ignore annotations that have already been found
     *
     * @param annotatedElement
     *         the annotated element
     * @param foundAnnotations
     *         the already found annotations
     */
    private static void findAllAnnotations(@NotNull final AnnotatedElement annotatedElement,
                                           @NotNull final Set<Class<? extends Annotation>> foundAnnotations) {
        Arrays.stream(annotatedElement.getDeclaredAnnotations())
                .map(Annotation::annotationType)
                .filter(annotation -> !foundAnnotations.contains(annotation))
                .forEach(aClass -> {
                    foundAnnotations.add(aClass);
                    findAllAnnotations(aClass, foundAnnotations);
                });
    }
}

Conclusion

This post has shown that with a little recursive implementation, meta annotations can easily be processed. In a real life scenario I would add caching mechanisms to make sure that the annotations for classes and methods are not repeatedly searched. They can be cached as they are created on compile time and do not change when running.

And, when you are already using Spring and just need to add some custom meta annotations to your project: Use Spring Core’s AnnotationUtil class and do not write something of your own!