behind the gist

thoughts on code, analysis and craft

Java annotation tutorial

Why use Annotations?

Annotations are a declarative way to extend class functionality. Other ways of extending class functionality are through inheritance, composition, the use of mixins or extra-language descriptions such as Aspect-Oriented Programming or other declarative markup. The tradeoff between these approaches is probably most important when designing frameworks. There has been a gradual shift away from using inheritance as the primary mechanism for extending frameworks since the added coupling to the framework is frequently a maintenance headache. In Java, the term POJO (plain old java object) is used to indicate a framework which can add functionality to plain java objects which do not have to extend or inherit from classes provided by the framework. Spring and Hibernate are popular examples of such frameworks. Hibernate is a object-relational-mapping framework which allows extension both through extra-language declarative markup (XML configuration files defining the mapping) and through annotations (both standard JSR-317 Java Persistence API annotations and proprietary ones).

The short answer is you should use annotations if you want to allow developers to add functionality to their code without changing the inheritance structure of their class tree. It is also popular to do so with external XML markup, but (for Java at least) development environments currently have better support for annotations. Java annotations are parsed in Eclipse and so the editor provides direct feedback when syntax errors exist. Annotations are also supported in refactorings, meaning changes to annotation classes or other class references will be automatically updated in the whole code base. This is not currently true for XML mappings - changes must be manually synchronized between the java code base and xml markup files. The benefit of an XML approach is that it does not require any changes to the original source code. This further reduces dependencies on the framework and also minimizes clutter in the classes that numerous annotations can sometimes bring.

The examples here are drawn from a framework based largely on JSR-311 Java API for RESTful Web Services.

Creating an Annotation

Defining an annotation in Java is very similar to designing an interface. Both define method signatures, but not their bodies. In the case of annotations, the methods cannot have parameters. The methods are essentially getters which the developer can use to determine how the annotation has been used in the code. Annotations also allow default return values. Finally, an annotation must itself be annotated to indicate how the annotation is to be used.

The following code uses the web services framework to define a createUser method. The annotations indicate that this method is invoked in response to a POST request and has three parameters. The parameters username and password are required while admin is optional.

1
2
3
4
5
@POST
public String createUser(
  @QueryParam(value="username",required=true) String userName,
  @QueryParam(value="password",required=true) String password,
  @QueryParam("admin") Boolean isAdmin)

The following is the definition of the @QueryParam annotation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface QueryParam {
  /**
   * Name of the query parameter
   */
  String value();

  /**
   * If true, an exception is raised if a request does not provide a value
   * for this parameter.
   */
  boolean required() default false;
}

The @Retention and @Target annotations on @QueryParam indicate the information is available at runtime and may only be applied to a method parameter. In annotations, the method name value is special. By creating a method named value, an annotation with just one argument (as is the case with the annotation on the isAdmin parameter above), then the name does not need to be provided when the annotation is used. Other method names, or cases where more than one parameter is provided, must be specified as in the annotations on userName and password.

Annotation methods may return arrays. The following example shows the use of an @UploadParam annotation which restricts the content types which can be uploaded.

1
2
3
4
  @POST @Path("container/{id}/upload")
  public static void uploadItem(
    @QueryParam("name") String name,
    @UploadParam(value="content", required=true, contentTypePrefixes="text/plain") FileItem file)

The following is the definition of @UploadParam

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface UploadParam {
  /**
   * Name of the path variable. 
   */
  String value();

  /**
   * If true, an exception is raised if a request does not provide a value
   * for this parameter.
   */
  boolean required() default false;

  /**
   * The list of valid content mime-type prefixes supported, e.g. "image"
   */
  String[] contentTypePrefixes() default {};
}

Note the use of the array initializer in the annotation definition but not where it is used. It is not required when specifying a single value, but would be required for the following example.

1
@UploadParam(value="content", required=true, contentTypePrefixes={"image/", "video/"}) FileItem file)

Extracting Annotation Information to Augment Runtime Behavior

Using annotations at runtime requires reflection. The following examples show how to process annotations at the class, method and parameter level. The following shows how various annotations are used for a UsersResource class to be used by the web services framework.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@Resource("/Users")
public class UsersResource {
  /**
   * Get a list of existing users.
   * @param like Partial string match on the user name.
   * @return List of {@link Serializer#userRep(User) user representations} in ascending order by user name
   * @example /Users?like=admin
   * @throws InsufficientPermissionException Request by a user without admin permissions.
   */
  @GET
  public String getUsers(
      @QueryParam("like") String like)
            {...}

  /**
   * Get a single user.
   * @param userId User identifier.
   * @return The requested {@link Serializer#userRep(User) user representation}
   * @throws InsufficientPermissionException A non-admin user requests a user id
   *         other than themselves.
   */
  @GET @Path("{userId}")
  public String getUser(
      @PathParam("userId") Long userId)
          {...}

  /**
   * Create a user.
   * 
   * @param userName Name for the new user.
   * @param password Password for the new user.
   * @param isAdmin Indicates if this new user has admin privileges.
   * @return The created {@link Serializer#userRep(User) user representation}.
   * @throws ParameterNotUniqueException The name of the new user is already taken.
   */
  @POST
  public String createUser(
      @QueryParam(value="username",required=true) String userName,
      @QueryParam(value="password",required=true) String password,
      @QueryParam("admin") Boolean isAdmin)
          {...}

  /**
   * Update user.
   * 
   * @param userId User identifier.
   * @param oldPassword Old password of the user (required if new password is provided and
   *        user does not have admin rights).
   * @param newPassword New password for the user.
   * @param isDisabled Disable/enable the user's account.
   * @param isAdmin Indicates if this new user has admin privileges.
   * @return The updated {@link Serializer#userRep(User) user representation}.
   * @throws InsufficientPermissionException A non-Admin user attempts to modify another user
   * @throws AuthenticationFailedException The old password does not match the provided value.
   */
  @PUT @Path("{userId}")
  public String updateUser(
      @PathParam("userId") Long userId,
      @QueryParam("username") String userName,
      @QueryParam("old_password") String oldPassword,
      @QueryParam("new_password") String newPassword,
      @QueryParam("disabled") Boolean isDisabled,
      @QueryParam("admin") Boolean isAdmin)
          {...}

  /**
   * Authenticate a user.
   * 
   * @param userId User identifier. 
   * @param userName Name of new or existing user, as above.
   * @param password Password of new or existing user, as above.
   * @return The authenticated {@link Serializer#userRep(User) user representation}
   * @throws AuthenticationFailedException Authentication failed.
   */
  @GET @Path("{userId}/Authenticate")
  public String authenticateUser(
      @PathParam("userId") Long userId,
      @QueryParam(value="username",required=true) String userName,
      @QueryParam(value="password",required=true) String password)
           {...}

}

The framework maps the following requests to the following method invocations

Request method
GET /Users getUsers(null)
GET /Users?like?smith getUsers("smith")
GET /Users/123 getUser(123)
POST /Users?username=bill&password=pwd createUser("bill", "pwd", false)
PUT /Users/123?old_password=pwd&password=newpwd updateUser(123, null, "pwd", "newpwd", null, null)
PUT /Users/123?admin=true updateUser(123, null, null, null, null, true)
POST /Users/123/Authenticate?username=bill&password=newpwd authenticateUser(123, "bill", "newpwd")

Class-Level Annotations

For the web services framework, each top-level web resource is annotated as a @Resource with the path template used to access the resource and potentially @Alias annotations indicating alternate paths. The corresponding annotation definitions are as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Resource {
  /**
   * Path template root used to access these types of resources.
   */
  String value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface Aliases {
  /**
   * List of absolute paths which should also invoke methods on this resource
   */
  String[] value();
}

The following is the code used to process the class-level annotations. In general, as much error handling as possible is performed in the annotation processing rather than method invocation since it is very difficult to interpret exceptions and debug reflection behavior at invocation time. Note the primary purpose of the code is to retrieve any framework-specific annotations on the class of interest, verify the annotation parameters, then process the methods defined within the class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void handleResource(Class<?> klass) {
  Resource resource = klass.getAnnotation(Resource.class);

  if (resource == null) {
    throw new PathIndexingException("class " + klass.getCanonicalName() +
    " does not have a Resource annotation");
  }

  if (!resource.value().startsWith("/")) {
    throw new PathIndexingException("Path values must be absolute in @Resource annotation for class " + klass.getCanonicalName());
  }

  ...

  Aliases aliases = klass.getAnnotation(Aliases.class);
  if (aliases != null) {
    for (String path : aliases.value()) {
      if (!path.startsWith("/")) {
        throw new PathIndexingException("Path values must be absolute in @Aliases annotation for class " + klass.getCanonicalName());
      }
      ...
    }
  }

  for (Method method : klass.getDeclaredMethods()) {
    handleMethod(klass, method, basePaths);
  }
}

Method-Level Annotations

The relevant method annotations @GET, @POST, @PUT and @Path are defined as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GET {
  /**
   * True if unsigned requests should be allowed for this resource.
   */
  boolean unsigned() default false;
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface POST {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PUT {
  /**
   * J2ME does not support PUT so by default allow a POST to
   * the same URI operate the same as a PUT.  Set to false if
   * a POST to the URI should have different semantics than
   * a PUT.
   */
  boolean allowPost() default true;
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Path {
  String value();
}

Processing the methods involves iterating through the annotations to determine which are relevant to the framework and handling them appropriately. Note that the code uses instanceof just as with interfaces to determine which annotation is present. Note that annotations do not behave polymorphically as method parameters so you need to use instanceof instead of something like the following.

1
2
handleAnnotation(GET getAnnotation) {...}
handleAnnotation(PUT putAnnotation) {...}

Also note that the annotation parameters are simply accessed by the methods used to define the annotations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
for (Annotation a : method.getAnnotations()) {
  if (a instanceof GET) {
    isUnsigned = ((GET)a).unsigned();
      ...
  }
  else if (a instanceof Path) {
    relativePath = ((Path)a).value();
    if (relativePath.startsWith("/")) {
       throw new PathIndexingException ("Path values cannot be absolute in @Path annotation for method " + klass.getCanonicalName() + "." + method.getName());
    }
  }
  ...
}

Parameter-Level Annotations

Parameter annotations work very similarly to method and class annotations. Parameter annotations have the additional property that you will typically want to check the type conformance of the annotated parameter (note this may also be true for method return values). You may want to handle objects and collections.

To do so, you use either Method.getGenericParameterTypes() or Method.getGenericReturnType() to get the desired Type values.

The following method will correctly determine if a parameter type matches (or derives from) a specified class.

1
2
3
4
5
6
7
8
9
private boolean isInstanceOf(Type type, Class<?> klass) {
  if (type instanceof Class<?>) {
    return klass.isAssignableFrom((Class<?>)type);
  }
  else if (type instanceof ParameterizedType) {
    return isInstanceOf(((ParameterizedType)type).getRawType(), klass);
  }
  return false;
}

To check that the parameter is of type Map<String,FileItem>, for example, the following code performs such a check

1
2
3
4
5
6
7
8
9
10
Type param = ...
if (param instanceof ParameterizedType) {
  ParameterizedType pType = (ParameterizedType) param;
  if (! (isInstanceOf(pType.getRawType(), Map.class) &&
    pType.getActualTypeArguments().length == 2 &&
    isInstanceOf(pType.getActualTypeArguments()[0], String.class) &&
    isInstanceOf(pType.getActualTypeArguments()[1], FileItem.class))) {
    throw new ParameterConfigurationException("parameter has @Xyz annotation, but is not conformable to Map<String,FileItem>");
  }
}

Finally, the following code snippet determines the item type of an array or collection (strictly only a List, Set or SortedSet)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Class<?> itemType = null;

// check to see if this is actually something like List<ModelType> or
// ModelType[] and get the real item type instead

Type returnType = method.getGenericReturnType();

if (returnType instanceof ParameterizedType) {
  // Handle types like Set<String>
  ParameterizedType pType = (ParameterizedType) returnType;
  if (pType.getRawType() instanceof Class<?>) {
    Class<?> rawType = (Class<?>)pType.getRawType();
    if ((List.class.isAssignableFrom(rawType) ||
      Set.class.isAssignableFrom(rawType) ||
      SortedSet.class.isAssignableFrom(rawType)) &&
      pType.getActualTypeArguments().length == 1 &&
      pType.getActualTypeArguments()[0] instanceof Class<?>) {
        itemType = (Class<?>)pType.getActualTypeArguments()[0];
      }
    }
  }
  else if (returnType instanceof Class<?>) {
    Class<?> cType = (Class<?>) returnType;
    if (cType.isArray()) {
      itemType = cType.getComponentType();
    }
    else {
      itemType = cType;
    }
  }
}

Invocation

Given the class, method and parameter annotations and definitions, the framework then has all the information needed to wrap the invocation of those methods with customized behavior. The specific processing details will vary based on the functionality of the framework. In the web services example, the framework handles servlet requests, converts string and upload parameters to typed values and invokes methods based on request parameters.

Other common uses for annotations are not related to method wrapping or invocation, but rather transform objects to different representations as is the case with many of the JPA annotations.

Extracting Annotation Information to Generate Documentation

Handling annotations in a doclet is a little different than handling them in the reflection code examples above. In particular, qualified name comparison is used to determine which annotations are present instead of instanceof. Also, annotation values are extracted as ElementValuePair values as in the following code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
for ( MethodDoc m : d.methods()) {
  ...
  for (AnnotationDesc a : m.annotations()) {
    if ("rest.annotation.Path".equals(a.annotationType().qualifiedTypeName())) {
      for (AnnotationDesc.ElementValuePair pair : a.elementValues()) {
        if ("value".equals(pair.element().name())) {
          relativePath = (String)pair.value().value();
        }
        else {
          throw new RuntimeException("don't understand Path annotation value " + pair.element().name());
        }
      }
    }
    if ("rest.annotation.GET".equals(a.annotationType().qualifiedTypeName())) {
      for (AnnotationDesc.ElementValuePair pair : a.elementValues()) {
        if ("unsigned".equals(pair.element().name())) {
          unsigned = (Boolean)pair.value().value();
        }
        else {
          throw new RuntimeException("don't understand GET annotation value " + pair.element().name());
        }
      }
    }
  }
}