Introduction
In the following chapters, I would like to introduce you to a development concept that helps solve traditional programming problems quicker and cleaner than it is possible with traditional techniques: Aspect Oriented Programming or AOP in combination with Java Annotations.
What is AOP?
The basic idea of Aspect Oriented Programming is to enable developers to express cross-cutting concerns in their code. A cross-cutting concern is a shared behavior or shared data that is used across the scope of an application.
The most commonly used example for AOP is the issue of logging. Logging is a cross-cutting behavior that is used by many separate parts of an application and intrudes the business logic of potentially many classes.
With the help of aspect oriented programming techniques, logging can be implemented as an aspect that is applied to all concerned classes without affecting their actual implementation. This is possible due to the fact that aspects are applied through rules, so-called pointcuts, that are evaluated separate from normal application execution.
The following example is a very simple logging aspect written in AspectJ and Java.
public aspect LoggingAspect {
pointcut logAnyMethodCall() :
call(* MyClass.*(..));
before() : logAnyMethodCall() {
System.out.println("I am invoked at '"
+ thisJoinPoint.getSourceLocation() + "'"); }
}
This aspect logs the source location of all code that calls a method on the class "MyClass". Admittedly, this is a very incomplete implementation but it clearly displays the main benefit of AOP: the original source code remains unchanged while logging is applied to it.
Disadvantages
The aforementioned behavior - the independent evaluation of pointcuts - sadly also leads directly to the major drawback of AOP: it is not clearly visible to the developer of an application which aspects will be applied to his or her code. Since an advice - the actual code that is invoked when a pointcut is triggered - can be woven into the code at any time, the behavior of an application is unpredictable even for the developer, unless he or she developed the aspects that are applied himself/herself.
It is quite obvious how an application that uses many aspects from many different developers can very quickly become a maintenance nightmare.
In the following chapters we will see how annotations can help us overcome this problem and allow us to improve our applications with aspects successfully.
Java annotations
Overview
Annotations allow a developer to add meta-information to his code, that means information that is not part of the operating code but represents useful data about the code. This allows the developer to mark a method for example as "performant", "slow" or - more usefully - "overridden" or "deprecated".
It is very important to understand that it is irrelevant at development time what this information means or what an annotated method actually does - the annotation can be evaluated by a component or application that is completely independent from the annotated code.
Annotations and aspects
Combining aspects and annotations leads to a truly powerful partnership for solving cross-cutting problems. The basic idea is to annotate methods, classes or members with meta-information and let aspects evaluate this information to enhance the code with additional features.
Since the annotations are logically separated from the code, the application will - and has to - run fine without them being evaluated.
However, if we chose to add functionality by applying an aspect, it does not come completely as a surprise to the developer: after all, he or she annotated the code with this information in the first place.
The logging example
Let's have another look at the logging example from before, this time modified to support annotations.
public aspect LoggingAspect {
pointcut logAnyMethodCall() :
call(@Logged * MyClass.*(..));
before() : logAnyMethodCall() {
System.out.println("I am invoked at '"
+ thisJoinPoint.getSourceLocation() + "'"); }
}
As you can see, the code is identical to before except for the highlighted changes.
Again, this aspect logs the source location of all code that calls a method on the class "MyClass", but this time only if the code is annotated with @Logged.
The following is a sample implementation of the Logged annotation from the aspect above.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Logged {}
This implementation guarantees that only methods can be annotated with @Logged.
Now, what is the advantage?
As opposed to the example from the last chapter, while this aspect can be applied to any Java application the pointcut will never be triggered unless the code is annotated with @Logged. Thus we do have to change our implementation before we can apply the aspect.
Does this sound like the aspect does not really give us any advantage over regular programming techniques?
Well, consider that the meta information we add to the original code does not change the behavior per se. We can add as many annotations as we want and the code will still compile and work as before.
Additionally, the normal development process would be slightly different. A developer writes a method and considers that it handles logic that requires logging. Thus he annotates the method with the Logged annotation. It is irrelevant whether he plans to develop a logging aspect or not and whether he maybe even implements his own logging functionality within this very method. All he does is adds meta-information to the method, telling everybody that it is applicable for logging.
To help understand this concept let us have a look at another example.
Application properties
The example
For this example, we are going to develop a properties management system that will allow us to work with application properties transparently and yet give us flexibility in the implementation of how to access these properties.
The easiest way for a developer to access properties is to put them as class members in the very class where he accesses them. Note that I am not saying it is the cleanest way, but the easiest. Naturally, every developer should feel a natural urge to not do this for all the good reasons we know so well: extensibility, reusability, etc.
However, we want to make life as easy as possible for us and aspects and annotations allow us to be lazy while still retaining all the flexibility we need.
First, let's examine the main class of our example.
package com.finalist.aprops;
public class Aprops {
private String applicationName = "Windows 95";
private static String APPLICATION_NAME = "Windows 98";
public static final void main(String args[]) {
Aprops aprops = new Aprops();
aprops.doIt();
System.out.println("Static name is '" + APPLICATION_NAME + "'");
System.out.println("Setting name to 'Ubuntu Linux'");
APPLICATION_NAME = "Ubuntu Linux";
System.out.println("Static name is '" + APPLICATION_NAME + "'");
aprops.doIt();
}
public void doIt() {
System.out.println("Instance name is '" + this.applicationName + "'");
}
}
This is all pretty straight forward and should not be too cryptic. We declare a class and an instance member and initialize them with default values. In the main method we create an instance and have it print the value of it's instance member. Then we print the value of the class member, set it to a new value and print it again. Afterwards, we let the instance print it's member's value again.
The only reason why we declare the member twice in this example is to show that the aspect works with both static and dynamic members and to show how modifying one of the properties affects all members.
The output when running the code above will be as follows:
Instance name is 'Windows 95'
Static name is 'Windows 98'
Setting name to 'Ubuntu Linux'
Static name is 'Ubuntu Linux'
Instance name is 'Windows 95'
As expected, the class member has changed after we set it while the instance member has not.
Dynamic property loading
Next step: the property loader. The implementation I present here is far from being perfect. It is a simple value object with an instance member and a fancily named getter and setter.
For our example this implementation is more then sufficient. However, I will leave it as an exercise to the reader to implement this properly: with properties stored in a local file, a database or even a JNDI folder.
package com.finalist.aprops;
public class PropertyLoader {
private String name = "Windows XP";
public String getProperty(String name) {
return this.name;
}
public void setProperty(String name, String property) {
this.name = property;
}
}
As visible from the code, we define a property with the value "Windows XP", which is going to be our application name. But how do we apply this to our main class above? How does it know that the class and instance members are referring to our application property in the property loader?
To do this, we first need to write an annotation that we can add to our members. Let's give it the verbose name Property.
package com.finalist.aprops.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Property {
String name();
String description() default "";
}
Whenever we want an aspect to evaluate an annotation at runtime, the retention policy of the annotation has to be set to "RUNTIME" - otherwise the annotation will be invisible to our aspect at application execution. Additionally, for this example, we specify "FIELD" as the only valid target for our annotation, which means that it can only be applied to class or instance members.
Furthermore, to give a little more information to whoever reads our annotation, we allow a property name and a description to be passed to the annotation. We use the property name as a unique identifier so that we can have multiple variables refer to the same application property while the description gives a possible reader a little hint on what we think this member represents.
So, let's go ahead and annotate our main class.
package com.finalist.aprops;
public class Aprops {
@Property(name = "applicationName",
description = "The name of the application")
private String applicationName = "Windows 95";
@Property(name = "applicationName",
description = "Also the name of the application")
private static String APPLICATION_NAME = "Windows 98";
public static final void main(String args[]) {
Aprops aprops = new Aprops();
aprops.doIt();
System.out.println("Static name is '" + APPLICATION_NAME + "'");
System.out.println("Setting name to 'Ubuntu Linux'");
APPLICATION_NAME = "Ubuntu Linux";
System.out.println("Static name is '" + APPLICATION_NAME + "'");
aprops.doIt();
}
public void doIt() {
System.out.println("Instance name is '" + this.applicationName + "'");
}
}
As you can see, the code is identical to the code before except for the annotation of both members of the class. Since we think both members represent the same property, both annotations have the same name and description.
A more sophisticated implementation of our simple property loader could now use this information to access the appropriate property for each member declared throughout the application. However, to keep this example as short as possible, we will not use the additional information associated with the annotation but simply return the one value our simplistic property loader has access to.
Developing the advice
Up until now, if we run the application again the output will be the same as before. Since we have not changed any of the operating code, the behavior of the application is unchanged.
It is time to write our aspect.
package com.finalist.aprops;
import com.finalist.aprops.annotation.Property;
public aspect PropertyAspect {
private static PropertyLoader propertyLoader = new PropertyLoader();
pointcut propertyRead(Property property) :
get(@Property * *) &&
@annotation(property) &&
!within(PropertyAspect) &&
!cflow(adviceexecution());
pointcut propertyWrite(Property property, String newValue) :
set(@Property * *) &&
@annotation(property) &&
args(newValue) &&
!cflow(staticinitialization(*)) &&
!cflow(initialization(*.new(..))) &&
!within(PropertyAspect) &&
!cflow(adviceexecution());
String around(Property property) :
propertyRead(property) {
return propertyLoader.getProperty(property.name());
}
before(Property property, String newValue) :
propertyWrite(property, newValue) {
propertyLoader.setProperty(newValue);
}
}
This is quite a handful for a start. But let's have a look at it in little chunks.
pointcut propertyRead(Property property) :
get(@Property * *) &&
@annotation(property) &&
!within(PropertyAspect) &&
!cflow(adviceexecution());
What we do here is define a pointcut called "propertyRead" that triggers on the event that a member annotated with @Property is accessed by a read operation. You can safely ignore the last three lines of the pointcut for now: they only tell the pointcut to export the values of the annotation for use in the actual aspect code and to not evaluate triggers that happen within the flow of the aspect itself.
pointcut propertyWrite(Property property, String newValue) :
set(@Property * *) &&
@annotation(property) &&
args(newValue) &&
!cflow(staticinitialization(*)) &&
!cflow(initialization(*.new(..))) &&
!within(PropertyAspect) &&
!cflow(adviceexecution());
Here, we define a pointcut called "propertyWrite" that triggers on the event that a member annotated with @Property is accessed by a write operation and we are not within the operational flow of static or instance initialization. Again, ignore the last five lines for now, they have the same behavior as above and additionally export the value that the member is being set to for use in the actual aspect code.
String around(Property property) :
propertyRead(property) {
return propertyLoader.getProperty(property.name());
}
Now it is getting interesting. This bit of code defines our first advice, the actual code that is run when a pointcut is triggered.
The "around" part tells AspectJ to execute the code around the invocation of the read operation. Within the advice code, we access our property loader 's getter for the value it has stored for the name of the property. And this is the value we return instead of the real value of the member.
That is correct: thanks to the "around" part of the advice we can actually change the value returned from the read operation on a member.
before(Property property, String newValue) :
propertyWrite(property, newValue) {
propertyLoader.setProperty(newValue);
}
}
Compared to what we did on a read operation, this is almost a little less fancy. We define an advice that reads the value the application tries to set the member identified by the pointcut to. This value we then pass to our property loader for storage: from now on, the property will have this value whenever we access it in the application.
Running the example again
When we apply this aspect to the application and run it again, the output should be as follows.
Instance name is 'Windows XP'
Static name is 'Windows XP'
Setting name to 'Ubuntu Linux'
Static name is 'Ubuntu Linux'
Instance name is 'Ubuntu Linux'
Now, this looks completely different to what we saw before.
And this is what happened:
Although we defined "Windows 95" as the default application name for the instance member, the aspect intercepts the call and replaces it with the value from our property loader. The same happens for the static member.
Additionally, since we added the meta-information that both members represent the same property, once we set the static member to a different value the instance member is affected as well: the change is persisted, application wide!
Conclusion
What we achieved with this excursion into AOP and annotations is that we have created the starting point for a framework where we can simply add application properties to a class by annotating a member with this information.
But what are the gains over traditional programming techniques?
Any developer working within an application can very easily specify his own application properties as members in his class. Simply by annotating them properly they can become dynamically loaded and application wide managed properties.
He can conveniently initialize the members with values that are valid for his environment and work with the application without ever having to apply aspects to his code. Only when the application has to run in different environments will the aspect be applied and the properties change accordingly.
However, since the developer annotates the members, he is aware that they can be modified. Thus there will be no finger pointing on the lines of "If I had known you are changing my variables with your aspects...".
Furthermore, the operational code is guaranteed to work just like it did before annotating any members as properties. There won't be any refactoring needs or compilation errors because the API of the properties loader changed.
By annotating his code, the developer supplies a contract, that - if honored by the aspect developer - allows for extremely flexible and versatile application development and simple and intuitive solutions for traditional programming problems while retaining a maintainable and clean code base.
No comments:
Post a Comment