Sunday, July 12, 2009

Annotated Java

Boilerplate code is a piece of code that is not deemed reusable enough to be abstracted into a function or a class, but it is useful enough to be copied and pasted. After pasting it, minor changes are applied to make it work in its new location. Over time, the boilerplate based code typically becomes a significant part of the source, hindering the cost-effective refactoring when the need arises. In some cases boiler plates can be removed by declarations in configuration files.
An annotation is a Java language feature that facilitates declarative programming at the source code level. Use this feature correctly and might end up with less boilerplate code, and less configuration files.
When creating your own annotation, you need to decide on the target and the retention policy of your annotation. The target specifies where the annotation can be declared; this could be on a type, a method, a field, a parameter, a constructor, a local variable or another annotation. The retention policy specifies when the annotation will be used. Annotations can be used on source level (ignored by the compiler), compiler level (ignored by the VM) or run time level.
An annotation can also be inherited or documented. When it is inherited, the values of the annotation on a super class is available on the sub class. The values of a documented annotation will be printed in the javadoc generated for the annotation class.

As an example, let's invent a simple scenario: You are implementing an API to you application data. The API is aware of the authenticated user and a set of security levels. The user is authorised to perform only certain methods on your API. The idea is to annotate a method with a security level. A user that does not have the assigned security level should not be able to invoke that method.
Assume we have an enum called SecurityLevel with three values BASIC, ADMINISTRATOR, SUPER. For our purposes we simply keep the security level as a property of the User, and the logged on user as a property of a Session singleton. The code of AnnotationDemo.java looks like this:
package com.willowsense;

import java.lang.annotation.*;
import java.lang.reflect.*;

enum SecurityLevel {
BASIC, ADMINISTRATOR, SUPER
}
class User {
final String login;
final SecurityLevel level;
public User(String login, SecurityLevel level) {
this.login = login;
this.level = level;
}
public SecurityLevel getLevel() {
return level; }

public String getLogin() {
return login;
}
}
class Session {
static private final Session instance = new Session();
static public Session getInstance() {
return instance; }
private User loggedInUser;

public void startSession(User user) {
loggedInUser = user;
}

public void endSession() {
loggedInUser = null;
}

public User getLoggedInUser() {
return loggedInUser;
}
}

The annotation needs a property of type SecurityLevel. For now we'll use this annotation during run time. The developer declares the annotation on a method, and we want the annotated values be shown on our javadocs. Let's call the annotation @Authorise. The code for the annotation and an example class that uses it follows:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Authorise {
SecurityLevel level();
}

class FinancialRepository {
@Authorise(level = SecurityLevel.SUPER)
public void deleteAccounts() {
AnnotationDemo.checkPermission();
// do the real work here
}
}


As you can see, there is some power in the declarative programming style. The author of the FinancialRepository class does not know or care about the details of how authorisation is implemented. However, he still needs to call the checkPermission() method. The checkPermission() method uses the stack trace and reflection to identify the calling method. It checks the annotation declared for that method against the logged in user's credentials. Have a look at the implementation in the code below. This code also contains a main method that shows how an authorisation error can be produced.

public class AnnotationDemo {
public static void checkPermission() {
StackTraceElement e[]=Thread.currentThread()
.getStackTrace();
int i;
for (i = 0; i < e.length; i++){
if (e[i].getClassName()
.equals(AnnotationDemo.class.getName())
&& e[i].getMethodName()
.equals("checkPermission"))
break;
}
Class<?> c;
try {
c = Class.forName(e[i+1].getClassName());
} catch (ClassNotFoundException e1) {
return;
}
String methodName = e[i+1].getMethodName();
for(Method m : c.getMethods()) {
if (m.getName().equals(methodName)) {
final Authorise auth =
m.getAnnotation(Authorise.class);
if (auth == null)return;
if (auth.level() != Session.getInstance()
.getLoggedInUser().getLevel())
throw new RuntimeException(
"Unauthorised access by "
+ Session.getInstance()
.getLoggedInUser().getLogin() +
" to " + c.getName() + "." + methodName);
}
}
}
static public void main(String[] args) {
try {
User u = new User("Rogue",SecurityLevel.BASIC);
Session.getInstance().startSession(u);
FinancialRepository r = new FinancialRepository();
r.deleteAccounts();
} catch (Exception ex) {
System.out.println(ex);
}
};
}

This sample code demonstrates how to implement an annotation at run time. Look out for places where annotations like these can be used effectively to reduce code and simplify your design.

0 comments:

Post a Comment