Narayanan Jayaratchagan is a Sun Certified J2EE Architect with more than six years of experience with Java and Microsoft Technologies.
What makes EJB components special is the declarative programming model through which we can specify the services such as security, persistence, transaction etc., that the container should provide. An EJB only implements the business logic; the services are associated through a deployment descriptor, which essentially acts as metadata for the EJB. At runtime, the container uses the metadata specified in the deployment descriptor to provide the services. The deployment descriptor is an XML file, not part of the Java classes that make up the EJBs. Is there a standard way to annotate the Java classes that make up the EJBs so that a developer can look at the class definition, together with annotations, and know everything about that class? It would be even better if the remote, home interfaces and the deployment descriptor could be automatically generated by a tool using the annotations. Better yet, can we provide the same kind of declarative services for a simple Java object? If so, how? This article examines how JSR-175: A Metadata Facility for the Java Programming Language will help us in finding answers to these questions and more.
Approaches to Programming
There are two approaches to programming called imperative programming and declarative programming. Imperative programming gives a list of instructions to execute in a particular order -- Java program that counts the number of words in a text file is an example of the imperative approach. Declarative programming describes a set of conditions, and lets the system figure out how to fulfill them. The SQL statement SELECT COUNT(*) FROM XYZ
is an example for the declarative approach. In other words, "specifying how" describes imperative programming and "specifying what is to be done, not how" describes declarative programming.
Annotations
The Tiger release of Java (JDK 1.5) adds a new language construct called annotation (proposed by JSR-175). Annotation is a generic mechanism for associating metadata (declarative information) with program elements such as classes, methods, fields, parameters, local variables, and packages. The compiler can store the metadata in the class files. Later, the VM or other programs can look for the metadata to determine how to interact with the program elements or change their behavior.
Declaring an Annotation
Declaring an annotation is very simple -- it takes the form of an interface declaration with an @
preceding it and optionally marked with meta-annotations, as shown below:
package njunit.annotation;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface UnitTest {
String value();
}
The Retention
meta-annotation declares that the @UnitTest
annotation should be stored in the class file and retained by the VM so it may be read reflectively. The Target
meta-annotation declares that the @UnitTest
annotation can be used to annotate methods in a Java class. @interface
declares the @UnitTest
annotation with one member called value
, which returns a String
.
Using an Annotation
Here is an example that shows how to use the @UnitTest
annotation declared in the previous section:
import njunit.annotation.*;
public class Example {
@UnitTest(value="Test 1. This test will pass.")
public void pass() {
assert 10 > 5;
}
@UnitTest("Test 2. This test will fail.")
public void fail() {
assert 10 < 5;
}
}
An annotation is applied to the code element by placing an annotation statement (@AnnotationType(...)
) before the program element. Annotation values take the form "name=value"
; for example, @UnitTest(value="some text")
. Single-member annotations with a member named value
are treated specially and can use the shorthand @UnitTest("some text")
. In the example, the @UnitTest
annotation is associated with the pass
and fail
methods.
Accessing Annotations at Runtime
Once annotations have been associated with program elements, we can use reflection to query their existence and get the values. The main reflection methods to query annotations are in a new interface: java.lang.reflect.AnnotatedElement
.
Methods available in the AnnotatedElement
interface are:
-
boolean isAnnotationPresent(Class annotationType)
Returnstrue
if an annotation for the specified type is present on this element, elsefalse
. This method is designed primarily for convenient access to marker annotations. -
T getAnnotation(Class annotationType)
Returns this element's annotation for the specified type if such an annotation is present, else null. -
Annotation[] getAnnotations()
Returns all annotations present on this element. (Returns an array of length zero if this element has no annotations.) -
Annotation[] getDeclaredAnnotations()
Returns all annotations that are directly present on this element. Unlike the other methods in this interface, this method ignores inherited annotations. (Returns an array of length zero if no annotations are directly present on this element.)
You may notice that the isAnnotationPresent
and getAnnotation
methods are defined using generics, another new feature available in JDK 1.5.
Here is the list of classes that implement the AnnotatedElement
interface:
java.lang.reflect.AccessibleObject
java.lang.Class
java.lang.reflect.Constructor
java.lang.reflect.Field
java.lang.reflect.Method
java.lang.Package
Next, I'll show you an example that illustrates how to access annotations at runtime.
package njunit;
import java.lang.reflect.*;
import njunit.annotation.*;
public class TestRunner {
static void executeUnitTests(String className) {
try {
Object testObject =
Class.forName(className).newInstance();
Method [] methods =
testObject.getClass().getDeclaredMethods();
for(Method amethod : methods) {
UnitTest utAnnotation =
amethod.getAnnotation(UnitTest.class);
if(utAnnotation!=null) {
System.out.print(utAnnotation.value() +
" : " );
String result =
invoke(amethod, testObject);
System.out.println(result);
}
}
}catch(Exception x) {
x.printStackTrace();
}
}
static String invoke(Method m, Object o) {
String result = "passed";
try{
m.invoke(o,null);
} catch(Exception x) {
result = "failed";
}
return result;
}
public static void main(String [] args) {
executeUnitTests(args[0]);
}
}
The TestRunner
uses the @UnitTest
annotation to determine whether a method is a unit test or not, invoke the method if it is marked with the @UnitTest
annotation, and report the success or failure.
Here is how the TestRunner
executes the unit test. Given a Java class, the TestRunner
first obtains the list of all declared methods using reflection. Then it queries each method using the enhanced for
construct and the getAnnotation
method available in JDK 1.5 to find out whether it is marked as a @UnitTest
. If it is marked, then it invokes the method and reports the success or failure. A test is considered failed if there is any exception when executing the test, and is considered passed otherwise.
In our Example
class, the pass
method will succeed when invoked, but the fail
method will throw an AssertionError
, which is propagated to the TestRunner.invoke
method as InvocationTargetException
.
When run with the command java -ea njunit.TestRunner Example
, the output looks like the following:
Test 1. This test will pass. : passed
Test 2. This test will fail. : failed
| |
Meta-Annotations
Java defines several standard meta-annotations (annotation types designed for annotating annotation type declarations).
The new package java.lang.annotation
contains the following meta-annotations:
Meta-annotation | Purpose |
---|---|
@Documented | Indicates that annotations with a type are to be documented by javadoc and similar tools by default. |
@Inherited | Indicates that an annotation type is automatically inherited. |
@Retention | Indicates how long annotations with the annotated type are to be retained. Example: @Retention(RetentionPolicy.RUNTIME) The enumeration RetentionPolicy defines the constants to be used for specifying Retention . |
@Target | Indicates the kinds of program element to which an annotation type is applicable. Example: @Target({ElementType.FIELD, ElementType.METHOD}) The enumeration ElementType defines constants that are used with the Target meta-annotation type to specify where it is legal to use an annotation type. |
Standard Annotations
There are two standard annotations in the java.lang
package.
Annotation | Purpose |
---|---|
@Deprecated | Reincarnation of deprecated javadoc tag as an annotation in JDK 1.5. Compilers warn when a deprecated program element is used or overridden in non-deprecated code. |
@Overrides | Indicates that a method declaration is intended to override a method declaration in a super-class. If a method is annotated with this annotation type but does not override a super-class method, compilers are required to generate an error message. |
The following example illustrates the use of standard annotations.
public class Parent {
@Deprecated
public void foo(int x) {
System.out.println("Parent.foo(int x) called.");
}
}
public class Child extends Parent {
@Overrides
public void foo() {
System.out.println("Child.foo() called.");
}
}
My intention is to extend the Parent
class and override the foo(int x)
method in Child
class. By mistake, the foo
method in the child does not override the one in the parent, because of the mismatch in the signature. Obviously, this is a bug. In the past, this kind of bug could be identified only at runtime after hours of debugging. Now, the use of the @Overrides
annotation can save hours wasted in debugging. When a method is annotated with @Overrides
, the compiler will check whether the method in a child class really overrides a method in the parent. The compiler will report an error when no method is overridden.
javac -source 1.5 Parent.java Child.java
Child.java:3: method does not override
a method from its superclass
@Overrides
^
1 error
Let's correct the error by modifying the foo
method signature, and compile the code again.
public class Child extends Parent{
@Overrides
public void foo(int x) {
System.out.println("Child.foo(int x) called.");
}
}
javac -Xlint:deprecation -source 1.5 Child.java
Child.java:4: warning:
[deprecation] foo(int) in Parent has been
deprecated
public void foo(int x) {
^
1 warning
The foo
method is marked as deprecated using the @Deprecated
annotation, so the compiler reports a warning, as shown above.
A Complex Example
To keep the introduction simple, I used a single valued annotation. Now it's time to look at a complex one.
public @interface Trademark {
String description();
String owner();
}
In the code snippet above we have declared an annotation called Trademark
with two members: description
and owner
, both of which return String
.
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.PACKAGE})
public @interface License {
String name();
String notice();
boolean redistributable();
Trademark[] trademarks();
}
In the code snippet above, we have declared an annotation called License
, which has members that return String
, boolean
, and an array of Trademark
annotations. Now we can use the License
annotation.
@License(
name = "Apache",
notice = "license notice ........",
redistributable = true,
trademarks =
{@Trademark(description="abcd",owner="xyz"),
@Trademark(description="efgh",owner="klmn")}
)
public class Example2 {
public static void main(String []args) {
License lic;
lic=Example2.class.getAnnotation(License.class);
System.out.println(lic.name());
System.out.println(lic.notice());
System.out.println(lic.redistributable());
Trademark [] tms = lic.trademarks();
for(Trademark tm : tms) {
System.out.println(tm.description());
System.out.println(tm.owner());
}
}
}
The above example shows how to use annotations that have multiple members with String
, boolean
, and array return types. It also illustrates how to define annotations with parameter values that are annotations. The main
method in Example2
illustrates how to access the annotations at runtime.
Defaults and Order of name=value
Pairs
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Help {
String topic() default "Unknown Topic";
String url();
}
Default values for members can be specified in the annotation declaration so that it becomes optional when defining it. In the above example, String topic() default "Unknown Topic";
declares a default value for topic
. While defining the Help
annotation, the topic
is optional and need not be specified. For example, @Help(url=".../help.html")
is valid. The url
must be specified, otherwise this will result in a compilation error.
In the annotation definition, the name=value
pairs can be specified in any order. @Help(topic="Order does not matter", url=".../help.html")
and @Help(url=".../help.html", topic="Order does not matter")
are considered the same.
| |
Package-Level Annotations
Annotations that can be applied to the package element are referred to as package-level annotations. An annotation with ElementType.PACKAGE
as one of its targets is a package-level annotation. Package-level annotations are placed in a package-info.java
file. This file should contain only the package statement, preceded by annotations. When the compiler encounters package-info.java
file, it will create a synthetic interface, package-name.package-info
. The interface is called synthetic because it is introduced by the compiler and does not have a corresponding construct in the source code. This synthetic interface makes it possible to access package-level annotations at runtime. The javadoc
utility will use the package-info.java
file if it is present, instead of package.html
, to generate documentation.
The package-info.java file will look like this:
/**
* documentation comments...
*/
@Annotation1(...)
package sample;
Note: The JDK 1.5 beta release compiler parses the package-info.java
file, but does not emit a synthetic interface as stated in the specification.
Restrictions on Annotations
It is important to know that certain restrictions are imposed on annotation declaration:
- No
extends
clause is permitted. (Annotation types automatically extend a new marker interface,java.lang.annotation.Annotation
.) - Methods must not have any parameters.
- Methods must not have any type parameters (in other words, generic methods are prohibited).
- Method return types are restricted to primitive types,
String
,Class
, enum types, annotation types, and arrays of the preceding types. - No
throws
clause is permitted. - Annotation types must not be parameterized.
Source-Level Annotations
Not all annotations need to be stored in the class files. For example, the deprecated
javadoc
tag used to mark a program element as obsolete and inform the compiler to emit a warning is now available as the Deprecated
annotation, which is a source-level annotation. To declare an annotation to be visible only at source level, use @Retention(RetentionPolicy.SOURCE)
. The compiler does not store source-level annotations in the class file. Source-level annotations are expected to be used by tools such as documentation generators (javadoc
), compilers, and other tools that require/have access to source files.
Accessing Annotations in the Source Files
We can use the Doclet API to access annotations from the source files. The com.sun.javadoc
package provides several new interfaces, and methods have been added to existing interfaces to support annotations. I'll walk you through an example that shows you how to access annotations using doclets.
import com.sun.javadoc.*;
public class ListAnnotations {
public static boolean start(RootDoc root) {
ClassDoc[] classes = root.classes();
for (ClassDoc clsDoc : classes) {
processAClass(clsDoc);
}
return true;
}
static void processAClass(ClassDoc clsDoc) {
System.out.println("List of annotations in " +
clsDoc.name());
list(clsDoc.annotations());
}
static void list(AnnotationDesc[] annDescs) {
for (AnnotationDesc ad : annDescs) {
AnnotationTypeDoc at = ad.annotationType();
System.out.println("----------");
System.out.println("Annotation : " + at.name());
AnnotationDesc.MemberValuePair [] members =
ad.memberValues();
for(AnnotationDesc.MemberValuePair mvp : members) {
System.out.println("Member = " +
mvp.member().name() +
", Value = "+ mvp.value() + "");
}
}
}
}
A Java class with the method public static boolean start(com.sun.javadoc.RootDoc root)
is a doclet. The doclet uses the interfaces from the com.sun.javadoc
package to gain access to various elements of a class from the source file. To execute a doclet, we have to use the javadoc
tool. javadoc
will parse the source files and call the start
method, which is the entry point for a doclet, similar to the main
method in a Java program.
Compiling and Executing the Doclet
javac -source 1.5 -cp c:\jdk15\lib\tools.jar ListAnnotations.java
javadoc -source 1.5 -doclet ListAnnotations -sourcepath . UnitTest.java
Loading source file UnitTest.java...
Constructing Javadoc information...
List of annotations in UnitTest
----------
Annotation : Retention
Member = value, Value = RUNTIME
----------
Annotation : Target
Member = value, Value = METHOD
Class-Level Annotations
We already saw two kinds of annotations: runtime and source. There is yet another kind of annotation: class. This is the default when the Retention
meta-annotation is not present or it is present with a value RetentionPolicy.CLASS
; the compiler stores annotations in the class files but the VM may not load them at runtime, so they may not be available for reflective access.
Attributes in .NET Vs. Annotations in Java
Microsoft's .NET CLR supports a feature called attributes, which is similar to annotations in Java. Let us see the similarities and differences between attributes and annotations.
.NET Attributes | Java Annotations |
---|---|
Attributes are classes that should extend the System.Attribute class. | Annotations are interfaces that automatically extend the java.lang.annotation.Annotation interface. |
Types of an attribute parameter must be primitive types, object, System.Type , enum types, or arrays of the preceding types. | Method return types are restricted to primitive types, String , Class , enum types, annotation types, and arrays of the preceding types. |
Attribute parameters can be named or positional. Named ("name=value" ) parameters can be in any order. | Supports only named ("name=value" ) parameters and can be in any order. Single member annotations follow the naming convention for the member type as value; can use shorthand. |
Attributes can be applied to assembly, class, constructor, delegate, enum, event, field, interface, method, module, parameter, property, return value, and struct. | Annotations can be applied to annotation, class, constructor, enum, field (includes enum constants), interface, local variable, method, parameter, and package. |
A declaration can have multiple attributes for the same attribute type. | A declaration cannot have multiple annotations for the same annotation type. |
Attributes are always stored in the assembly and available at runtime. | Annotations marked with @Target(RetentionPolicy. SOURCE) meta-annotation (referred to as source-level annotations) are not stored in the class files and will not be available at runtime. |
All attributes can be accessed at runtime using reflective APIs. | Annotations marked with @Target(RetentionPolicy.RUNTIME) meta-annotation (referred to as runtime annotations) can be accessed at runtime through reflective APIs. |
System.CodeDOM APIs for working with attributes in source files. | Doclet API provides limited support for working with annotations at source files. |
.NET provides standard attributes for security, transactions, language interoperability, COM integration, COM+ hosting, marshalling, serialization, components and controls, assemblies and packaging, threading, XML and web services. | Several new JSRs are in progress to define standard annotations for web services and others. |
Context attributes provide an interception mechanism that can preprocess and postprocess class instantiation and method calls. | N/A |
For developers familiar with .NET attributes, the JDK 1.5 implementation of annotations may appear to be a limitation, but it is not. Java annotations can be used to define design-time information, such as documentation or instructions to the compiler; runtime information, such as tagging a method as a unit test; or behavioral characteristics, such as whether a member is participating in a transaction or not, similar to how attributes are used in .NET applications. Although there is no standard interception mechanism like .NET's context attributes in Java yet, AOP frameworks such as AspectJ and JBossAOP provide call interception mechanisms, and these frameworks may be reimplemented to take advantage of annotations.
Annotations in the Pre-JDK 1.5 World
In older versions of Java, javadoc
custom tags are used as annotations. Many popular open source design-time tools, such as XDoclet, EJBGen, and others, generate code based on javadoc
tags form the source files. Open source projects like Jakarta Commons Attributes and Attrib4j use javadoc
tags to associate custom metadata and make it available at runtime. In both Commons Attributes and Attrib4j, attributes are classes more like .NET attributes, whereas JDK 1.5 treats them as interfaces. For those who can't start using JDK 1.5 as soon as the production release of Tiger is available, it's worthwhile to take a look at Retroweaver. Retroweaver transforms JDK 1.5 classes into classes that can run on older VMs; this way, you can start using JDK 1.5 features with an older version of the JRE.
Conclusion
Every new release of Java has introduced new features, but few warrant a new way of thinking to realize their full potential. Using annotations effectively to simplify programming in Java requires a shift in our thought processes. Even though we use declarative programming languages such as SQL and XSLT most frequently, it may take some time for us to understand how to use declarative and imperative programming together.
Here are a few things you could do with annotations to simplify or empower your programming:
- Develop code generators using annotations.
- Develop a new unit-testing framework using annotations, such as NUnit.
- Think about ways to extend the compiler using custom annotation handlers.
- Think about annotation's role in defining AOP extensions to Java.
Once JSRs like JSR-181: Web Services Metadata for the Java Platform and others are implemented, we will be able to appreciate the productivity gained by using annotations.
In the near future, we will see a number of existing frameworks being reimplemented using annotations, generics and other new features available in JDK 1.5, codenamed Tiger. So get ready to meet the Tiger.
(http://www.onjava.com/lpt/a/4768)
No comments:
Post a Comment