Make Your Code Work for You with Java Annotations

Monday Dec 15th 2008 by David Thurmond
Share:

Use Java annotations to take the grunt work out of coding.

Annotations are a familiar sight to any Java developer who has ever read the Java API docs. They are the text snippets preceded by an @ sign that have been appearing in JavaDoc comments since Java was introduced. They may seem like simple snippets of extra information, but there is much more to annotations than meets the eye. Here, you will learn how to use annotations at coding time, compile time, and run time to make the most of your development efforts.

What Are Annotations?

An annotation is a snippet of text in a JavaDoc comment that provides a little extra information. For example, listing 1 shows a method that is discouraged from continued use as indicated by the @Deprecated annotation.

Listing 1: A sample annotation in code

/**
 * @Deprecated
 * This method does something that was super duper ten years ago.
 */
public void doSomething() {
}    // doSomething()

When Javadocs are generated for this method, it is flagged as deprecated, and will appear in the Deprecated section of your generated API docs. Also, at compile time, you will receive a warning that you are using a method that is no longer recommended for use.

Although this useful functionality has been around for many years, the real power of annotations was unleashed in JDK 1.5 when the annotations API was made available to developers. For the first time, developers could write their own annotations. This made it possible for developers to create their own home-grown annotations, opening the way for the creation of powerful development tools.

One such tool is the ubiquitous JUnit testing framework. JUnit version 4.5 takes advantage of its own set of annotations to allow developers to set up, execute, and tear down test cases simply by placing annotations within their code. For details on JUnit 4.5 and how annotations are used to perform automated unit testing, see my article, "Test Cases Made Easy with JUnit 4.5."

Next, I will discuss how to create your own annotations, and how to use them at coding time, compile time, and run time to create your own development tools.

Creating an Annotation

First things first; before you can create any powerful new development tools, you will need to create annotations to use in your code. Listing 2 shows the simplest annotation you can create. This annotation, @ExecTime, is referred to as a tagging annotation, because it does not allow any parameters to be specified as part of the annotation.

Listing 2: ExecTime.java

package com.dlt.developer.annotation;

import java.lang.annotation.*;

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

This code is just an interface with no business logic built into it. The interface has no methods; this indicates that the annotation will not accept any parameters, similar to the @Deprecated annotation shown earlier.

The @interface annotation in this code indicates that the interface is itself to be an annotation. It is necessary for the java.lang.annotation package to be imported before the @interface annotation and the other annotations shown here can be used without generating compile-time errors.

The next interesting thing to note about the ExecTime annotation in Listing 2 is this line:

@Retention(RetentionPolicy.RUNTIME)

This line indicates that the @ExecTime annotation is to be compiled into the class files and should be made available to the Java VM at run time. This makes it possible for development tools to look for the @ExecTime annotation through reflection at run time and to perform a specific task because the annotation was present in the code.

The other possible values for the @Retention annotation are RetentionPolicy.SOURCE and RetentionPolicy.CLASS.

  • The RetentionPolicy.SOURCE constant indicates that the annotation should be discarded by the compiler, and is usable by any development tools at coding time only.
  • The RetentionPolicy.CLASS constant indicates that the annotation is useful to the compiler, and should be retained in the .class file for use by the compiler at compile time. The@SuppressWarnings and @Overrides annotations are examples of compile-time annotations. These annotations tell the compiler to behave differently based on the presence of these annotations within the source code. For example, if a method is tagged with the @Overrides annotation, the compiler will generate an error if the method signature doesn't match the overridden method's signature.

Unless you are writing your own Java compiler, it is not possible to create your own useful compile-time annotations. New compile-time annotations would instead be introduced to support new language features or enhancements to the Java compiler.

The other noteworthy part of ExecTime.java is the line:

@Target(ElementType.METHOD)

This line indicates that the annotation should appear only on a method declaration. Placing the @ExecTime annotation on a class declaration or constructor, for example, would generate a compiler error. It is possible to specify that an annotation may appear on one or more elements within a Java program, including on constructors, fields, local variables, method declarations, package declarations, parameter declarations; and class, interface, or enumeration declarations.

The ExecTime.java program demonstrates the basics of creating an annotation. Listing 3 shows the code for a more complex annotation, @Help, which accepts parameters.

Listing 3: Help.java

package com.dlt.developer.annotation;

import java.lang.annotation.*;
@Target({ElementType.CONSTRUCTOR,
         ElementType.FIELD,
         ElementType.METHOD,
         ElementType.PACKAGE,
         ElementType.TYPE})

@Retention(RetentionPolicy.SOURCE)
public @interface Help {
   int contextID();
   String topic();
   String text();
}    // interface Help

The @Help annotation allows the programmer to specify three parameters: a context ID, a help topic, and text. Each parameter corresponds to a method declared in the annotation's interface. Valid return values for these methods include primitive data types, such as int, double, char, Boolean, and so on; strings, classes, enumerations, annotations, and arrays of any of these types. Annotation parameter methods are not allowed to accept any arguments, nor are they allowed to throw any exceptions. Doing either will result in a compiler error.

Note that the @Retention annotation in Listing 3 indicates that the @Help annotation's information is available to any development tools only at coding time. Also, note that the @Target annotation indicates that a @Help annotation can appear on any Java program element.

A method that uses the @Help annotation might look like the one shown in Listing 4.

Listing 4: Using @Help annotation in code

/**
 * @Help (contextID=99, topic="How To Do Something Terrific",
 *        text="When you invoke the doSomething() method,
 *        something awesome might happen.")
 */
public void doSomething() {
}    // doSomething()

The method declaration above shows that the parameters specified in parentheses correspond to the method names in the annotation interface declared earlier. The order of the parameters does not matter, but the data type for each parameter must correspond to the data type of the corresponding method in the annotation interface; otherwise, a compiler error will be generated. Note that strings must be enclosed in quotes as shown.

Listing 5 shows one last bit of razzle-dazzle that can be used when creating your own annotations: allowing some parameters to be optional.

Listing 5: MemoryCheck.java

package com.dlt.developer.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MemoryCheck {
   public static final String B  = "B";
   public static final String KB = "KB";
   public static final String MB = "MB";

   String size() default KB;
}    // interface MemoryCheck

This annotation only accepts one parameter, size, which is optional, and will default to KB if it is not specified, as shown by the "default" keyword after the method declaration.

Annotations serve no purpose unless they are used by development tools, so the following sections show how to create tools that use the annotations created above at coding time and run time.

Using Annotations at Coding Time

Coding can be a lot of work, but remember the old saying, "Work smarter, not harder.W Annotations are a great way to take the drudgery out of repetitive coding tasks, such as coding Enterprise Java Beans' local and remote interfaces, bean descriptor files, JSP taglib files, wrapper classes, and so on.

Rather than focus on J2EE technologies that may not be familiar to all readers, I will demonstrate how to write a very simple help XML file using the @Help annotation shown earlier.

Listing 6 shows HelpGenerator.java, which looks through the code for the @Help annotation and generates an XML tag for each method that has the annotation. This program could be enhanced to account for help entered for constructors, fields, and other program elements that allow the @Help annotation as well.

Listing 6: HelpGenerator.java

package com.dlt.developer.helpgenerator;

import java.io.*;

import com.sun.javadoc.*;

public class HelpGenerator {

   public static boolean start(RootDoc root){
      System.out.println("Running help generator...");
      try {
         writeContents(root.classes());
      } catch (Exception e) {
         System.out.println("Error generating help.");
         System.out.println(e.getMessage());
         e.printStackTrace();
      }    // catch
      return true;
   }    // start()


   private static void writeContents(ClassDoc[] classes)
      throws Exception {
      System.out.println("Processing class doc...");
      for (int i=0; i < classes.length; i++) {
         System.out.println(classes[i].name());
         StringBuffer outBuff = new StringBuffer();
         outBuff.append("<Help class=\"" + classes[i].name() +
            "\">");
         PrintWriter out = 
            new PrintWriter(new FileOutputStream("help_" +
            classes[i].name() + ".xml"));
         MethodDoc[] methods = classes[i].methods();
         for (int j=0; j < methods.length; j++) {
            MethodDoc theMethod = (MethodDoc)methods[j];
            System.out.println("Method " + theMethod.name());
            AnnotationDesc[] annotations = theMethod.annotations();
            for (int k=0; k < annotations.length; k++) {
               AnnotationDesc theAnnotationDesc = annotations[k];
                  AnnotationTypeDoc theAnnotationType =
                  theAnnotationDesc.annotationType();
               System.out.println(theAnnotationType.name());
               AnnotationDesc.ElementValuePair[] valuePairs =
                  theAnnotationDesc.elementValues();
               String contextID = "";
               String text = "";
               String topic = "";
               for (int l = 0; l < valuePairs.length; l++) {
                  AnnotationDesc.ElementValuePair thePair =
                     (AnnotationDesc.ElementValuePair)valuePairs[l];
                  AnnotationTypeElementDoc theElement =
                     thePair.element();
                  AnnotationValue theValue = thePair.value();
                  System.out.println("Element=" + theElement.name() +
                     " Value=" + theValue.toString());
                  if (theAnnotationType.name().equals("Help")) {
                     if (theElement.name().equals("contextID")) {
                        contextID = theValue.toString();
                     } else if (theElement.name().equals("text")) {
                        text =
                           theValue.toString().replaceAll("\"", "");
                     } else if (theElement.name().equals("topic")) {
                        topic =
                           theValue.toString().replaceAll("\"", "");
                     }    // if
                  }       // if
               }          // for l
               if (theAnnotationType.name().equals("Help")) {
                  outBuff.append("<HelpTopic contextID=\"");
                  outBuff.append(contextID);
                  outBuff.append("\" topic=\"");
                  outBuff.append(topic);
                  outBuff.append("\" text=\"");
                  outBuff.append(text); outBuff.append("\"/>");
               }    // if
            }       // for k
         }          // for j
         outBuff.append("</Help>");
         out.print(outBuff.toString());
         out.flush();
         out.close();
      }    // for i
   }       // writeContents()
}          // class HelpGenerator

HelpGenerator.java is an example of a doclet. Doclets are used by the Javadoc tool to control the output that is generated when Javadocs are generated from source code. In this case, rather than generating HTML API documentation, this doclet will generate a file called help_<class-name>.xml that contains all of the help topics defined in the source code with the @Help annotation.

The first method declared in the HelpGenerator class is:

public static boolean start(RootDoc root)

which is the standard entry point for all doclets. The Javadoc tool invokes this method to begin any processing that is to be done using a custom doclet. This method accepts a RootDoc as a parameter, which then allows the doclet to gain access to all of the source code files to be processed.

The doclet then grabs a list of all of the ClassDoc objects to be processed. The i for-loop creates a new help file corresponding to each ClassDoc object processed. A ClassDoc is the doclet equivalent of a Java class, and contains all of the documentation elements for the Java source code for the class. Next, an array of MethodDoc objects is obtained from the ClassDoc, and the j for-loop cycles through each one. Within the j for-loop, all of the annotations found in each MethodDoc are retrieved. The AnnotationDesc object corresponds to an occurrence of an annotation.

Next, in the k for-loop, each annotation is echoed to System.out, and the annotation's parameter name-value pairs are obtained. This is the meat of the help topic XML tags. For each name-value pair, the l for-loop cycles through and determines whether the parameter is the context ID, help topic, or text, and stores it in the correct variable. Then, the help topic XML tag is created and written to the output file.

The l for-loop obtains each annotation's parameters and prints them out as well. Within this loop, if the annotation is the @Help annotation, each parameter value is placed into a variable for outputting to the help XML tag to be generated.

To run the program, you must invoke the Javadoc tool as follows:

"%java_home%"\bin\javadoc
   -doclet com.dlt.developer.helpgenerator.HelpGenerator
   -docletpath classes
   -sourcepath src com.dlt.developer.numbers

The Javadoc tool accepts several parameters. The first is the –doclet parameter that tells Javadoc to use the HelpGenerator custom doclet. The next is the –docletpath parameter that tells Javadoc where to find the doclet class. Next is the –sourcepath parameter, which tells Javadoc where to find the source code to be processed. Finally, the package name for which to execute Javadoc is specified. In this case, it refers to com.dlt.developer.numbers, which contains NumberCruncher.java, a source code file with several @Help annotations included. The output from running this program can be found in the help_NumberCruncher.xml file that is included in the examples file in the "Download the Code" section.

It is easy to see how a custom doclet could be created to do any number of code generation tasks. The doclet API includes documentation equivalents for all of the Java language constructs, making it easy to access annotations that go along with each one.

Using Annotations at Run Time

Now that I have covered coding-time and compile-time annotations, I will cover perhaps the most exciting application of annotations: run-time development tools! Using the @ExecTime and @MemoryCheck annotations discussed earlier, I will demonstrate how annotations and reflection can be used to create a run-time development tool that profiles execution time and memory usage for the methods on a Java class in Listing 7.

Listing 7: Profiler.java

package com.dlt.developer.profiler;

import java.lang.reflect.Method;
import com.dlt.developer.annotation.MemoryCheck;
import com.dlt.developer.annotation.ExecTime;

public class Profiler {
   public final static String PROFILE_MEMORY = "-mem";
   public final static String PROFILE_TIME   = "-time";
   public final static String PROFILE_ALL    = "-all";

   private Object theInstance = null;
   private String theProfile  = PROFILE_ALL;

   public Profiler(String className, String profile) {
      try {
         theInstance = Class.forName(className).newInstance();
      } catch (Exception ex) {
         System.out.println("Cannot instantiate class due to
                             exception " + ex);
         System.exit(1);
      }    // catch
      if (!(profile.equals(PROFILE_MEMORY) ||
            profile.equals(PROFILE_TIME) ||
            profile.equals(PROFILE_ALL))) {
         doUsage();
         System.exit(1);
      }    // if
   }       // Profiler()

   public static void doUsage() {
      System.out.println("");
      System.out.println("Usage:");
      System.out.println("$java Profiler MyClass <option>");
      System.out.println("where <option> must be one of:");
      System.out.println("-mem = Show memory usage for all
                          methods");
      System.out.println("-time = Show execution time for all
                          methods");
      System.out.println("-all = Show all profile information
                          for all methods");
      System.out.println("");
   }    // doUsage()

   public void doProfile() throws Exception {
      Method [] methods =
         theInstance.getClass().getDeclaredMethods();
      for(int i = 0; i < methods.length; i++) {
         Method theMethod = methods[i];
         MemoryCheck theMemCheckAnnotation =
            theMethod.getAnnotation(MemoryCheck.class);
         ExecTime theExecTimeAnnotation =
            theMethod.getAnnotation(ExecTime.class);

         long freeMemoryBefore = 0;
         long freeMemoryAfter  = 0;

         long timeBefore = 0;
         long timeAfter  = 0;

         if (theMemCheckAnnotation != null &&
            (theProfile.equals(PROFILE_MEMORY) ||
            theProfile.equals(PROFILE_ALL))) {
               System.gc();
               freeMemoryBefore = Runtime.getRuntime().freeMemory();
         }    // if

         if (theExecTimeAnnotation != null &&
            (theProfile.equals(PROFILE_TIME) ||
            theProfile.equals(PROFILE_ALL))) {
               timeBefore = System.currentTimeMillis();
         }    // if

         if (theMemCheckAnnotation != null ||
            theExecTimeAnnotation != null) {
               System.out.println("Profiling " +
               theMethod.getName());
               theMethod.invoke(theInstance, null);
         }    // if

         if (theMemCheckAnnotation != null &&
            (theProfile.equals(PROFILE_MEMORY) ||
            theProfile.equals(PROFILE_ALL))) {
            freeMemoryAfter = Runtime.getRuntime().freeMemory();
            double memoryUsed = freeMemoryBefore - freeMemoryAfter;
            if (theMemCheckAnnotation.size().equals(MemoryCheck.KB)) {
               memoryUsed = memoryUsed / 1024;
            } else if (theMemCheckAnnotation.size().
               equals(MemoryCheck.MB)) {
               memoryUsed = memoryUsed / 1024000;
            }    // if
            System.out.println("Profiler: The memory required to
               execute " + theMethod.getName() + " for class " +
               theInstance.getClass().getName() + " was " +
               memoryUsed + theMemCheckAnnotation.size());
         }    // if

         if (theExecTimeAnnotation != null &&
            (theProfile.equals(PROFILE_TIME) ||
            theProfile.equals(PROFILE_ALL))) {
            timeAfter = System.currentTimeMillis();
            System.out.println("Profiler: The time required to
               execute " + theMethod.getName() + " for class " +
               theInstance.getClass().getName() + " was " +
               (timeAfter - timeBefore) + " milliseconds");
         }    // if
      }       // for i
   }          // doProfile()

   public static void main(String[] args) throws Exception {

      if (args.length != 2) {
         doUsage();
         System.exit(1);
      }    // if

      System.out.println("Beginning profiling of class " + args[0]);
      Profiler theProfiler = new Profiler(args[0], args[1]);
      theProfiler.doProfile();
      System.out.println("Done!");
   }    // main()

}       // class Profiler

Profiler.java accepts two parameters: a switch to indicate whether memory, execution time, or both are to be profiled; and the class name whose methods are to be profiled.

First, in the profiler's constructor, the profiler creates an instance of the class specified on the command line so that methods can be invoked on it. Next, the profiler obtains the class's methods through reflection:

Method [] methods = theInstance.getClass().getDeclaredMethods();

Next, the profiler cycles through all of the class's methods. For each method, the profiler checks to see if the @MemoryCheck or @ExecTime annotations were present, and then invokes the method:

theMethod.getAnnotation(MemoryCheck.class);
...
theMethod.getAnnotation(ExecTime.class);
...
if (theMemCheckAnnotation != null || theExecTimeAnnotation != null) {
   System.out.println("Profiling " + theMethod.getName());
   theMethod.invoke(theInstance, null);
}    // if

Note that the method's getAnnotation() method will return null if the specified annotation class was not present on the method declaration. If either the @ExecTime or @MemoryCheck annotation is not null, the method is invoked so that the before and after profile values can be computed.

  • If the @ExecTime annotation is present, the profiler notes the time in milliseconds before the method invocation, the time in milliseconds afterward, and calculates how many milliseconds execution took.
  • If the @MemoryCheck annotation is present, the profiler notes the free memory before the method invocation, the free memory afterwards, and calculates how much memory was used. Depending on the parameter value on the @MemoryCheck annotation, the memory used is shown in bytes, KB, or MB.

This profiler is only a very simple example of how to use annotations at run time, and it could certainly be improved. For one thing, the careful observer will note that it is impossible to profile a method that has any parameters at present. However, you can see that it is not difficult to access the annotations that were present in the source code by using reflection. The possibilities for the tools you can create are limited only by your imagination.

Conclusion

Annotations used at coding time can reduce repetitive coding tasks dramatically. When used at run time, they provide a way for meta-tools to access important information provided in your code. Hopefully, I have sparked your imagination and encouraged you to use this powerful Java language feature to make your code work hard for you.

Download the Code

The complete code from this article can be downloaded here.

About the Author

David Thurmond is a Sun Certified Developer with over fifteen years of software development experience. He has worked in the agriculture, construction equipment, financial, home improvement, and logistics industries.

Share:
Home
Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved