@Repeatable
In the maturation of Java’s type system, annotations have emerged as a primary mechanism for declarative configuration, compile-time processing, and runtime behavior injection. Since their formal introduction, they have powered everything from Spring’s component scanning to JAX-RS routing and JPA mapping. Yet, for developers who cut their teeth on early Java SE 8 releases, a persistent frustration was the language’s inability to apply the same annotation type more than once to a single declaration. Code that desired multiple configurations—such as listing several interceptors, defining numerous scheduled triggers, or tagging a class with several metadata tags—required the cumbersome pattern of a container annotation wrapping an array of individual instances. This not only inflated boilerplate but also obscured the developer’s intent beneath layers of indirection.
The arrival of @Repeatable alongside Java 8 addressed this gap directly. As a meta-annotation, @Repeatable instructs the compiler to treat multiple applications of the annotated type as a coherent collection, transparently maintaining an internal array while allowing source code to read as a simple, repeated usage. This section explores the mechanics of declaring, using, and consuming repeatable annotations, and explains why the feature remains a cornerstone of modern Java library design.
Defining a Repeatable Annotation
Creating a repeatable annotation is a three-part harmony: a container annotation, the repeatable annotation itself, and the @Repeatable marker that binds them. The container serves as the runtime holder for multiple instances. It is a standard annotation whose value() method returns an array of the component annotation type. The repeatable annotation carries the @Repeatable marker, pointing to its container class. Both typically share the same retention and target policies to ensure binary compatibility and seamless reflection.
Step 1: The Container Annotation
The container annotation does not carry business logic itself; its purpose is to aggregate repeated instances. Its declaration mirrors any other annotation, except that one of its elements is an array of the component type. For example, if modeling recurring tags on a document entity, the container would declare Tag[] value(). The container is typically annotated with @Retention(RetentionPolicy.RUNTIME) so that its contents are accessible via reflection.
Step 2: The Repeatable Annotation
Here, @Repeatable steps in. Applied directly to the annotation being made repeatable, it references the container class via its value() element. The Java compiler, upon successful compilation, rewrites repeated usages into a single container annotation encompassing an array of the original type. This transformation is pure syntactic sugar; the bytecode still relies on the container, but the source code remains clean.
Step 3: Usage
With the infrastructure in place, developers can now write source code that reads naturally:
@Tag(name = "security")
@Tag(name = "performance")
public class ServiceEndpoint { ... }
The compiler ensures both @Tag instances are preserved under the hood as @Tags({ @Tag("security"), @Tag("performance") }). The source developer, however, interacts with the repeated form, enjoying a significant boost in readability and maintainability.
Retrieving Repeatable Annotations at Runtime
At runtime, the reflection API provides two paths for accessing repeatable annotations. The modern, type-safe approach uses Class.getAnnotationsByType(), which returns an array of the repeatable annotation regardless of how many times it was applied. The legacy getAnnotation() method, by contrast, returns null if the annotation was used more than once, making it unsuitable for repeatable types without manually unwrapping the container.
Consider a utility that scans classes for all declared tags:
Tag[] tags = clazz.getAnnotationsByType(Tag.class);
for (Tag t : tags) {
System.out.println(t.name());
}
This prints each tag in sequence, abstracting away the container entirely. Developers should internalize that getAnnotationsByType is the gateway to repeatable annotations; any code path that relies on getAnnotation for a repeatable type risks silent failures or missing data.
Practical Guidelines
Target Alignment: The
@Targetof the container should be at least as broad as that of the repeatable annotation. If the inner annotation targets methods, the container typically targets types or methods as well, depending on the use case. Mismatched targets can lead to unexpected retention or application failures.Retention Consistency: Both annotations should declare
RetentionPolicy.RUNTIMEif runtime reflection is intended. A compile-time-only retention will render the repeatable structure invisible at runtime, defeating the purpose of the feature.Backward Compatibility: Since
@Repeatableis syntactic sugar, the generated bytecode still depends on the container class. Library authors must ensure both the component annotation and its container are publicly exported, especially in API distributions. Omitting the container from public signatures can break client code that attempts introspection.
Conclusion
@Repeatable exemplifies how a seemingly small meta-annotation can dramatically improve developer ergonomics. By eliminating the boilerplate of manual container annotations, it enables cleaner, more declarative code while preserving full reflection capabilities. Whether you're designing a framework, configuring bean metadata, or tagging domain objects, integrating @Repeatable into your annotation toolkit is a mark of modern Java sensibility. It not only reduces verbosity but also aligns the source code more closely with the developer's mental model, bridging the gap between expressive intent and compiler reality.