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 |
|
The following is the definition of the @QueryParam
annotation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
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 |
|
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 |
|
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
|
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|