Intended audience: Java (6+) developers who write annotations.
You've probably come across Java annotations which are constrained in some way (they are incompatible with one ore more other annotations, require a no argument constructor on the annotated class, etc.), but most of the time these constraints are only mentioned in the annotation's JavaDoc and enforced at runtime. However, most of these constraints could be verified at compile-time if there was a way to express them, and odds are that you'd prefer compile-time errors to runtime ones since you are already using Java.
annotation-constraints is a library for Java 6 or newer that allows you to specify constraints on annotations which are verified at compile-time via the included annotation processor. It includes commonly-used constraint meta-annotations and allows you to create your own. Additionally, it allows you to add constraints to existing (e.g. third-party) annotations.
For example, suppose you had a @Model
annotation which should only be placed on a class which extends AbstractModel
and has a no-argument constructor. You could add some constraint meta-annotations to it like so:
@TargetMustHaveSupertypes(AbstractModel.class) //target must extend AbstractModel
@TargetMustHaveConstructors(@Constructor({})) //target must have a no-arg constructor
@Target(ElementType.TYPE)
public @interface Model {
}
These constraints are validated at compile-time when annotation-constraints is on the compiler's classpath. No configuration is necessary because it includes an annotation processor which is picked up automatically by javac (see below for Eclipse usage). If you violate any of the constraints, you'll receive an error. For example:
@Model
public class Person {
private final String name;
public Person(String name) {
this.name = name;
}
}
Compiling the Person
class results in two compilation errors:
Class Person is annotated with @Model but does not have AbstractModel as a supertype
Class Person is annotated with @Model but does not have a constructor with no arguments
The following constraints are included in the com.overstock.constraint
package. They can be combined with one another
and/or with your own custom constraints. The phrase target annotation below refers to the annotation which is being
constrained (i.e. annotated with one or more of these constraint meta-annotations). In the example above, @Model
is
the target annotation because it is annotated with @TargetMustHaveSupertypes
and @TargetMustHaveConstructors
. The
phrase target element below refers to the program element which is annotated with the target annotation, e.g.
Person
above.
- @TargetCannotBeAnnotatedWith(Class<? extends Annotation[]) issues an error when the target element is annotated with both the target annotation and any of the incompatible annotations. This is a way of specifying that the target annotation is not compatible with the specified annotations.
- @TargetShouldBeAnnotatedWith(Class<? extends Annotation[]) issues a warning when the target element is annotated with the target annotation and not with all of the specified annotations.
- @TargetMustBeAnnotatedWith(Class<? extends Annotation[]) issues an error when the target element is annotated with the target annotation and not with all of the specified annotations.
- @TargetMustHaveASupertypeAnnotatedWith(Class<? extends Annotation[]) is the same as
@TargetMustBeAnnotatedWith
except it checks supertypes (i.e. for annotations which are not@Inherited
). - @TargetMustHaveConstructors(Constructor[]) issues an error when the target element is annotated with the target annotation and does not have all of the required constructors with the necessary arguments types.
- @TargetMustHaveSupertypes(Class<?>[]) issues an error when an target element is annotated with the target annotation and does not have all of the required supertypes (classes and/or interfaces).
You may want to add a constraint meta-annotation to some annotation for which you don't control the source code. Here's how to do just that.
- Create a new annotation and add constraint meta-annotations to it.
- Annotate your new annotation with
@ProvidesConstraintsFor(ExistingAnnotation.class)
. - To register your new annotation with annotation-constraints, create a text file named
com.overstock.constraint.provider.constraint-providers
underMETA-INF
with the fully-qualified binary class name of your new annotation in it. Without this file, the constraints will only be validated in the compilation unit in which they're defined. - Make sure the annotation-constraints jar and your new annotation class are on the classpath during compilation.
See the JavaDoc for com.overstock.constraint.provider.ProvidesConstraintsFor for more details.
For example, JAX-RS (JSR 311) has @ApplicationPath
, which is required to only be applied to a subclass of
Application
. To have this validated at compile-time we would do the following.
First, create an annotation on which to put constraints. The name or location of this annotation doesn't really matter, so let's call it ApplicationPathConstraints:
package example;
import ...
@Target({}) //this annotation is not intended to be placed on any program element
@Retention(RetentionPolicy.RUNTIME)
@ProvidesConstraintsFor(ApplicationPath.class)
@TargetMustHaveSupertypes(Application.class)
public @interface ApplicationPathConstraints {
}
Next, create a text file named META-INF/com.overstock.constraint.provider.constraint-providers with the following line of text:
example.ApplicationPathProvider
That's it. As long as these files are in the current compilation unit or on the classpath during compilation, the validation will occur at compile-time.
If you need a constraint which is not provided, you can write your own meta-annotation and a Verifier
for it.
Though there is some overlap, we think writing a Verifier
is easier than writing an annotation processor from scratch.
- Create an annotation and add
@Constraint(verifiedBy = ...)
to it. - Implement the
Verifier
for your new constraint. (See the JavaDoc for com.overstock.constraint.verifier.Verifier for more details and/or have a look at an example Verifier.) - Make sure both annotation-constraints and your new
Verifier
class are on the classpath during compilation.
Note: Custom Verifier
s cannot be executed in the same compilation unit in which they are declared (which makes sense
because they have yet to be compiled). This does not prevent Verifier
s from being declared in the same compilation
unit as the annotation(s) they verify, it only prevents them from being exercised against that same compilation unit.
Suppose that you had several web service projects using JAX-RS (JSR 311) annotations and you wanted to reserve a certain path for health checks, say "/health", across all web services. To implement this, you would:
- Create a new constraint annotation, @ReservedPaths.
- Implement a new verifier, ReservedPathVerifier.
- In this case, since we're adding a constraint an existing annotation we need to create a provider, PathConstraintProvider. This is only necessary if you're not able to add the constraint to the annotation's source code.
Then, if you have a class which uses a reserved path, you get a compilation error similar to:
verifier.ReservedPathFail is annotated with @Path using a reserved path: /health
annotation-constraints runs as an annotation processor, which happens automatically when it's on the classpath at compile-time (for Java 6 and greater). No extra configuration is necessary other than declaring a dependency on annotation-constraints.
<dependencies>
...
<dependency>
<groupId>com.overstock</groupId>
<artifactId>annotation-constraints</artifactId>
<version>${annotation-constraints.version}</version>
</dependency>
...
</dependencies>
If you use Maven, the easiest way to use annotation-constraints within Eclipse is using m2eclipse and m2e-apt.
- Install m2eclipse from the Eclipse Marketplace.
- Install m2e-apt (from the Eclipse Marketplace or from the update site listed here.
- Import your project or right-click and under Maven choose Update Project... and the m2e-apt configurator will configure annotation processors based on the project's Maven classpath.
If you're not using Maven you'll have to configure annotation processing in Eclipse by hand.
- Under the project's properties, go to Java Compiler -> Annotation Processing and check "Enable project specific settings", "Enable annotation processing" and "Enable processing in editor".
- Under Annotation Processing, go to Factory Path and add the annotation-constraints jar via Add JARs..., Add External JARs or Add Variable....
- Also add any jars which contain additional constraints (custom constraints or
@ProvidesConstraintsFor
) along with any jars which they depend on.