Taming Tiger, Part 3

Decorate your code with Java annotations

Welcome to the third and final part of this three-part series on Sun Microsystems' latest release of the Java 2 Platform, Standard Edition (J2SE). As I mentioned in Part 1, J2SE 5 (also called Tiger) is the most significant revision to the Java language since its original inception and has extended Java with several new features. To refresh your memory, Part 1 provided a brief introduction to J2SE 5 and covered many new additions to the Java language. Part 2 was devoted entirely to generics, which are Java's counterpart to templates in C++ and a similar facility (also called generics) in C# 2.0. In this final installment, I focus exclusively on the newly introduced metadata feature in J2SE 5 called annotations. Annotations allow programmers to decorate Java code with their own attributes. These attributes can be used for code documentation, code generation, and, even during runtime, for providing special services such as enhanced business-level security or special business logic.

Read the whole series: "Taming Tiger," Tarak Modi (JavaWorld):

What is metadata?

If you look up the definition of metadata, the dictionary will tell you it's data about data. The same definition applies to metadata in Java as well. Metadata is not a completely foreign concept to Java. Java has always supported a (very) limited implementation of metadata via its javadoc tags. An example code fragment follows:

/**
 * @author Tarak Modi
 *
*/
public final class AnnotationsTest
{
}

In the above code fragment, the @author tag is an example of metadata for the AnnotationsTest class. In other words, AnnotationsTest is annotated or decorated by the author metadata. Currently, this metadata is only used by tools such as javadoc and is not even available during runtime. XDoclet, an open source project, is another example that uses javadoc-style metadata tags to annotate and generate code based on the metadata.

With J2SE 5, the Java team has taken Java's metadata capability to a new level. The concept is formally called annotations. In addition to the built-in annotations included in Version 5, you can also use custom annotations that you define to decorate types (such as classes, interfaces, and enums), methods and constructors, and fields. You can control the availability of these annotations as well. For example, some annotations may be available only in the source code; others may be available in the compiled class files as well. Some annotations may even be available during runtime in the JVM.

Built-in annotations

J2SE 5 comes with six prebuilt annotations. Each annotation is described below. The annotation's actual definition is also provided in the reference's description. Two points to note from the definitions are their simplicity and that the annotations themselves are annotated. Later in this article, I walk you through a complete example of building and using your own annotation; during that discussion, I explain the declaration's syntax.

The following are the six built-in annotations in J2SE 5:

java.lang.Overrides

@Target(ElementType.METHOD)
public @interface Overrides 
{
}

This annotation is used to indicate that a method declaration in the current class is intended to override a method declaration in its (direct or indirect) superclass. If a method is decorated with this annotation type but does not override a superclass method, then the compiler will generate an error message. As an example, consider the following code fragment:

class A
{
@Overrides
    public String toString(int i)
    {
        return "";
    }
}

Compiling this code fragment produces the following error during compilation:

method does not override a method from its superclass
                @Overrides
                 ^

To fix the error, change the method signature from public String toString(int i) to public String toString(). Using this annotation is a good way to catch inadvertent programming errors during compilation, where you think you are overriding a method, but in reality you are creating a new one.

java.lang.annotation.Documented

@Documented
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented 
{
}

This annotation indicates that the annotation to which this is applied is to be documented by javadoc and similar tools. Note that this annotation is just a hint, and a tool can ignore the annotation if it desires to do so.

java.lang.annotation.Deprecated

@Documented
@Retention(RetentionPolicy.SOURCE)
public @interface Deprecated 
{
}

This annotation provides a hint to the Java compiler to warn users if they use the class, method, or field annotated with this annotation. Typically, programmers are discouraged from using a deprecated method (or class), because either it is dangerous or a better alternative exists.

As an example of when you could use the Deprecated annotation, consider a scenario where you have implemented a sort() method on a class used by many other developers on different projects. Later you discover a shortcoming in the code and not only have to rewrite the function, but also change its signature. You can't remove the old sort() method because other programs rely on it. What you can do is annotate it as being deprecated so when developers work on and compile their code, they will be advised to use the new sort() method instead of the old one.

java.lang.annotation.Inherited

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited 
{
}

The best way to understand this annotation is through an example. Let's say you created your own annotation called Serializable. A developer would use your annotation if his class implemented the Serializable interface. If the developer created any new classes that derived from his original class, then marking those classes as Serializable would make sense. To force that design principal, you decorate your Serializable annotation with the Inherited annotation. Stated more generally, if an annotation A is decorated with the Inherited annotation, then all subclasses of a class decorated by the annotation A will automatically inherit the annotation A as well.

java.lang.annotation.Retention

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention 
{
    RetentionPolicy value();
}

The Retention annotation takes a single parameter that determines the decorated annotation's availability. The values of this parameter are:

  1. RetentionPolicy.SOURCE: The decorated annotation is available at the source code level only

  2. RetentionPolicy.CLASS: The decorated annotation is available in the source code and compiled class file, but is not loaded into the JVM at runtime

  3. RetentionPolicy.RUNTIME: The decorated annotation is available in the source code, the compiled class file, and is also loaded into the JVM at runtime

By default, all annotations are available at the source level only.

java.lang.annotation.Target

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target 
{
    ElementType[] value();
}

This annotation is used to indicate the type of program element (such as a class, method, or field) to which the declared annotation is applicable. If the Target annotation is not present on an annotation type declaration, then the declared type may be used on any program element. If the Target annotation is present, then the compiler will enforce the specified usage restriction. Legal values for the Target annotation are contained in the java.lang.annotation.ElementType enumeration. An annotation can be applied to any combination of a class, a method, a field, a package declaration, a constructor, a parameter, a local variable, and another annotation.

Create a custom annotation

It's easy to create and then use a custom annotation. As you have already seen above, the declaration of an annotation is straightforward. In this section, I walk you through a complete exercise of creating, applying, and dynamically querying a custom annotation during runtime.

The custom annotation

Let's create an annotation that can be applied to classes and methods and enforces role-based security. The declaration is shown below:

@Documented
@Target({METHOD,TYPE})
@Retention(RUNTIME) 
public @interface SecurityPermission
{
    String[] value();
}

We can glean the following information from the declaration:

  1. The annotation is to be documented by tools such as javadoc.
  2. The annotation can be applied only to methods and types such as classes.
  3. The annotation should be available during runtime.
  4. The annotation is called SecurityPermission.
  5. The annotation has one member called value and is an array of string. Note that we don't declare the actual member, only an accessor method to retrieve it.

The next step is declaring a class that uses our new annotation. An example is shown below:

@SecurityPermission("All")
public class AnnotationsTest
{
    public AnnotationsTest()
    {
        SecurityBlanket.checkPermission();
    }
    @SecurityPermission("All")
    public void unRestrictedMethod(String message)
    {
        SecurityBlanket.checkPermission();
        System.out.println("Message from unRestrictedMethod: " + message);
    }
    @SecurityPermission("None")
    public void fullyRestrictedMethod(String message)
    {
        SecurityBlanket.checkPermission();
        System.out.println("Message from fullyRestrictedMethod: " + message);
    }
    @SecurityPermission("Manager")
    public void partiallyRestrictedMethod(String message)
    {
        SecurityBlanket.checkPermission();
        System.out.println("Message from partiallyRestrictedMethod: " + message);
    }
    @SecurityPermission({"Manager","HR"})
    public void partiallyRestrictedMethod2(String message)
    {
        SecurityBlanket.checkPermission();
        System.out.println("Message from partiallyRestrictedMethod2: " + message);
    }
}

As you can see, AnnotationsTest is a class just like any other with special javadoc-like tags (@SecurityPermission) above the class and method declarations. This is how you apply the SecurityPermission annotation. Note that this is also the same way you apply the built-in annotations to Java code, such as, for example, to the SecurityPermission annotation declaration. Next, look at the various SecurityPermission annotation declarations to see how I specified the value parameter's value.

As you peruse through the AnnotationsTest class above, you may wonder what the call SecurityBlanket.checkPermission() is doing. This is where all the "magic" occurs. Simply declaring a custom annotation and then decorating your code with it is not going to accomplish much. The Java runtime has no idea what to do with your custom annotations. They just occupy valuable memory space doing nothing. That's where the SecurityBlanket.checkPermission() method call fits in.

First, this function figures out what the currently executing method is. Next, the function determines if the caller's role is one of the permitted roles for the method. If the method is a class constructor, then the function uses the roles specified in the SecurityPermission annotation applied to the class declaration; otherwise it uses the roles specified in the SecurityPermission annotation applied to the method declaration. The caller's roles are specified on a per-thread basis by calling the SecurityBlanket.addPermission method. In a real application, a custom JAAS (Java Authentication and Authorization Specification) module that allows a user to log into your application may call the SecurityBlanket.addPermission method on your behalf.

The point to realize from all this is that J2SE 5 provides us with a way of declaring our own custom annotations and a way of decorating our own code with them. It also loads them in the JVM and makes them available dynamically and reflectively. But then we have to write the glue logic that actually makes something useful happen.

The entire SecurityBlanket class is shown below:

public class SecurityBlanket
{
    private static Logger logger = Logger.getLogger("AnnotationsTest");
    private static HashMap<Thread, String> permissions = new HashMap<Thread, String>();
    public static void addPermission(String s)
    {
        permissions.put(Thread.currentThread(),s);
    }
    public static void removePermission()
    {
        permissions.remove(Thread.currentThread());
    }
    public static void checkPermission()
    {
        StackTraceElement e[]=Thread.currentThread().getStackTrace();
        int i = 0;
        for (i = 0; i <e.length; i++)
        {
            if (e[i].getClassName().equals("SecurityBlanket") 
                && e[i].getMethodName().equals("checkPermission"))
                break;
        }
        if (i == e.length)
        {
            throw new RuntimeException("Unexpected Security Error.");
        }
        logger.info("Checking security access to " 
                    + e[i+1].getClassName() 
                    + "::" 
                    + e[i+1].getMethodName()); 
        try
        {
            Class c = Class.forName(e[i+1].getClassName());
            if (e[i+1].getMethodName().equals("<init>"))
            {
                SecurityPermission permission = 
                    (SecurityPermission)
                        c.getAnnotation(SecurityPermission.class);
                if (permission != null)
                {
                    String currentRole = 
                        permissions.get(Thread.currentThread());
                    for (String role:permission.value())
                    {
                        if (role.equals("All"))
                            return;
                        else if (role.equals("None"))
                        {
                            throw new RuntimeException(
                                "Unauthorized access to class " 
                                   + e[i+1].getClassName());
                        }
                        if (role.equals(currentRole))
                            return;
                    }
                }
                return;
            }
            Method[] methods = c.getMethods();
            for (Method m:methods)
            {
                if (m.getName().equals(e[i+1].getMethodName()))
                {
                    SecurityPermission permission = 
                        m.getAnnotation(SecurityPermission.class);
                    if (permission != null)
                    {
                        String currentRole = 
                            permissions.get(Thread.currentThread());
                        for (String role:permission.value())
                        {
                            if (role.equals("All"))
                                return;
                            else if (role.equals("None"))
                            {
                                throw new RuntimeException(
                                    "Unauthorized access to " 
                                        + e[i+1].getClassName() 
                                        + "::" 
                                        + e[i+1].getMethodName());
                            }
                            if (role.equals(currentRole))
                                return;
                        }
                    }
                    break;
                }
            }
            throw new RuntimeException("Unauthorized access to " 
                                       + e[i+1].getClassName() 
                                       + "::" 
                                       + e[i+1].getMethodName());
        }
        catch (ClassNotFoundException ex)
        {
            logger.severe("Got an error: " + ex.getMessage()); 
            throw new RuntimeException("Unexpected Security Error.");
        }
    }
} 

The bulk of the code is straightforward. However, note the use of the getAnnotation() method on the java.lang.reflect.Method and the java.lang.Class classes. These are just two of the many new methods added to support reflectively retrieving annotations during runtime.

Finally, to see everything in action, let's add a main() method to the AnnotationsTest class. The main() method creates two threads. Each thread simulates a different user with a different role and calls all four public instance methods in the AnnotationsTest class. The main method is shown below:

public static void main(String[] args)
    {
        try
        {
            Thread t1 = new Thread(new Test1());
            t1.start();
            t1.join();
            Thread t2 = new Thread(new Test2());
            t2.start();
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

And here are the Test1 and Test2 classes used by the main method:

class Test1 implements Runnable
{
    public void run() 
    {
         // Add the "HR" role for this user 
        SecurityBlanket.addPermission("HR");
        try
        {
            AnnotationsTest test = new AnnotationsTest();
            try
            {
                test.unRestrictedMethod("Hi");
            }
            catch (Exception ex)
            {
                System.out.println(ex.getMessage());
            }
            try
            {
                test.fullyRestrictedMethod("Hi");
            }
            catch (Exception ex)
            {
                System.out.println(ex.getMessage());
            }
            try
            {
                test.partiallyRestrictedMethod("Hi");
            }
            catch (Exception ex)
            {
                System.out.println(ex.getMessage());
            }
            try
            {
                test.partiallyRestrictedMethod2("Hi");
            }
            catch (Exception ex)
            {
                System.out.println(ex.getMessage());
            }
        }
        finally
        {
            SecurityBlanket.removePermission();
        }
    }
}
class Test2 implements Runnable
{
    public void run() 
    {        
        // Add the "Manager" role for this user
        SecurityBlanket.addPermission("Manager");
        try
        {
            AnnotationsTest test = new AnnotationsTest();
            try
            {
                test.unRestrictedMethod("Hi");
            }
            catch (Exception ex)
            {
                System.out.println(ex.getMessage());
            }
            try
            {
                test.fullyRestrictedMethod("Hi");
            }
            catch (Exception ex)
            {
                System.out.println(ex.getMessage());
            }
            try
            {
                test.partiallyRestrictedMethod("Hi");
            }
            catch (Exception ex)
            {
                System.out.println(ex.getMessage());
            }
            try
            {
                test.partiallyRestrictedMethod2("Hi");
            }
            catch (Exception ex)
            {
                System.out.println(ex.getMessage());
            }
        }
        finally
        {
            SecurityBlanket.removePermission();
        }
    }
}

When you run the AnnotationsTest class, you will see the following output:

Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::<init>
Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::unRestrictedMethod
Message from unRestrictedMethod: Hi
Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::fullyRestrictedMethod
Unauthorized access to AnnotationsTest::fullyRestrictedMethod
Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::partiallyRestrictedMethod
Unauthorized access to AnnotationsTest::partiallyRestrictedMethod
Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::partiallyRestrictedMethod2
Message from partiallyRestrictedMethod2: Hi
Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::<init>
Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::unRestrictedMethod
Message from unRestrictedMethod: Hi
Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::fullyRestrictedMethod
Unauthorized access to AnnotationsTest::fullyRestrictedMethod
Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::partiallyRestrictedMethod
Message from partiallyRestrictedMethod: Hi
Mar 20, 2004 4:46:55 PM SecurityBlanket checkPermission
INFO: Checking security access to AnnotationsTest::partiallyRestrictedMethod2
Message from partiallyRestrictedMethod2: Hi

Observe the following in the output:

  1. The first thread (role set to "HR") has access only to the unRestrictedMethod() and partiallyRestrictedMethod2() methods.
  2. The second thread (role set to "Manager") has access to the unRestrictedMethod(), partiallyRestrictedMethod(), and partiallyRestrictedMethod2() methods.
  3. Neither thread has access to the fullyRestrictedMethod() method.
  4. Both threads can access the unRestrictedMethod() method.
  5. Both threads were able to create an instance of the AnnotationsTest class because the class was annotated with the SecurityPermission annotation SecurityPermission("All"). If All was changed to None, then neither thread could have created an instance of the class.

The best way to see what's going on is to play around with the code and change the values of the various SecurityPermission annotations that are sprinkled around the AnnotationsTest class.

Finally, an awesome exercise would be to use AspectJ to define and implement the appropriate pointcuts that would allow you to completely remove the manual call to SecurityBlanket.checkPermission() from your code. Now that would be real "magic," and AspectJ makes it easy.

Conclusion

This concludes my three-part series on J2SE 5. Along the way, I have covered many interesting new language features, some more useful than others. I talked at length about the new Java metadata facility called annotations in this article. Annotations provide us with a host of new capabilities. Expect to see many tools in the next couple of years that capitalize on Java annotations to provide new value-added services either at compile time by code modification or at runtime.

Tarak Modi has been architecting scalable, high-performance, distributed applications for more than eight years and is currently a senior specialist with North Highland, a management and technology consulting company. His professional experience includes hardcore C++ and Java programming; working with Microsoft technologies such as COM, MTS, COM+, and more recently .Net; Java-based technologies including J2EE; and CORBA. He has written many articles in well-known .Net and Java publications including several in JavaWorld. He currently hosts the Java Design blog at JavaWorld. He is also coauthor of Professional Java Web Services (Wrox Press, 2002; ISBN 1861003757). To find out more about him, visit his personal Website at http://www.tekNirvana.com.
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more