|
DEVELOPER: AOP
Taking Abstraction One Step Further
By James Holmes
Reduce coding time and duplication with aspect-oriented programming.
One of the hottest new concepts in programming today is aspect-oriented programming (AOP). Once used primarily
in academia and research-and-development organizations, AOP
today is making inroads into mainstream development. Using AOP is an evolutionary way of developing software that improves upon object-oriented programming (OOP), in much the same way that OOP improved upon procedural programming. OOP introduced the concepts of encapsulation, inheritance, and polymorphism for creating a hierarchy of objects that model a common set of behaviors. OOP falls short, however, in providing a means of handling common behaviors that extend across unrelated objects. That
is, OOP allows you to define top-down relationships but is not well suited for left-to-right relationships. For example, consider logging. Logging code is often scattered horizontally across object hierarchies and has nothing to do with the core functions of the objects it's scattered across. The same is true for other types of code, such as security, exception handling, and transparent persistence. This scattered and unrelated code is known as cross-cutting code and is the reason for AOP's existence.
AOP provides a solution for abstracting cross-cutting code that spans object hierarchies without functional relevance to the code it spans. Instead of embedding cross-cutting code in classes, AOP allows you to abstract the cross-cutting code into
a separate moduleknown as an aspectand then apply the code dynamically where it is needed. You achieve dynamic application of the cross-cutting code by defining specific placesknown as pointcutsin your object model where cross-cutting code should be applied. At runtimeor compile time, depending on your AOP frameworkcross-cutting code is injected at the specified pointcuts. Essentially, AOP allows you to introduce new functionality into objects without the objects' needing to have any knowledge of that introduction. This is a very powerful concept.
To further understand how AOP works, consider a typical AOP logging example. Listing 1 shows two simple objectsObjectA and ObjectBthat contain logging code.
With standard object-oriented programming, every time you need it, you must code logging into the appropriate objects. In the example in Listing 1, using System.out.println() calls for logging proves to be tedious. An alternative to using System.out.println() calls is to use logging frameworks such as log4j, but this strategy creates extra overhead and creates unnecessary clutter in the classes in which it is used. Whether the logging uses System.out.println() calls or a logging framework, the logging code has no functional relevance to the classes in which it is embedded.
With AOP, you can dynamically insert logging code into the classes
that need it. This way, objects can focus on their core responsibilities. Java objects that focus only on their core responsibilites are often referred to as POJOs: Plain Old Java Objects. You can use AOP
to add logging and several other types of common functionality
to POJOs, freeing them from unnecessarily embedding unrelated functionality.
This article introduces and explains AOP terminology, explains the differences between AOP frameworks, and then
steps through a sample use
of AOP for caching.
AOP Terminology
AOP introduces several new terms for describing its fundamental concepts. A solid understanding of these terms is necessary to understanding AOP. The following is a list of terms introduced by AOP, and their descriptions:
1. AdviceThis is the code that is applied to, or cross-cuts, your existing object model. Advice code is what modifies the behavior or properties of an existing object. Advice is also commonly referred to as introductions or mix-ins.
2. PointcutsThese define the points in your model where advice will be applied. For example, pointcuts define where in a class code should be introduced or which methods should be intercepted before they are executed. Pointcuts are also commonly referred
to as joinpoints.
3. AspectsThese package advice and pointcuts into functional units in much the same way that OOP uses classes to package fields and methods into cohesive units. For example, you might have a logging aspect that contains advice and pointcuts for applying logging code to all setter and getter methods on objects.
Selecting a Framework
Before getting started with AOP, it's necessary to select an AOP framework to use. Neither Java nor any of the other dominant object-oriented programming languages has built-in support for AOP. Several AOP frameworks are available for Java, however, most of which share the same core functionality, but they are differentiated in the way AOP is tied into an object model.
Some AOP frameworks use bytecode manipulation to tie into an object model, and others use proxy-based systems to tie in. The frameworks that use the bytecode approach either modify source code before it is compiled into bytecode or modify bytecode after it has been compiled. Both approaches effectively yield the same result: modified bytecode that weaves AOP into the original code. Proxy-based frameworks use a proxy system whereby the AOP framework intercepts all method invocations to the aspected objects and then proxies the method calls to their intended objects. This proxying is transparent and thus allows bytecode to be left unchanged.
Another core differentiation between AOP frameworks is in the
way aspects are defined and applied. With some frameworks, aspects are defined and applied with code, whereas others require you to define and apply aspects with an XML configuration file. Better still, some frameworks support defining and applying aspects in code as well as with XML configuration files.
Because most AOP frameworks
have overlapping functionality, deciding which framework to use often comes down to the unique features
of a particular framework. The AOP sample application in this article
was developed with dynaop, a new AOP framework. The principles of
the sample application, however, are generic and apply to all AOP frameworks. The dynaop framework supports defining and applying aspects with code; it also offers a unique
solution for the definition and
application of aspects with the BeanShell scripting framework. BeanShell scripts allow you to easily script Java objects, as the sample
application shows.
Putting AOP to Use with dynaop
A simple caching aspect that caches the results of methods illustrates the basic concepts of AOP. The first call to a method that has the caching aspect applied invokes the method and caches its result. Subsequent calls to that aspected method return the method's results from the cache. This basic caching aspect is especially useful
for long-running methods, such as methods that crunch a lot of data
or methods that issue several queries
to a data source.
As is the case in the logging example in Listing 1, embedding the caching functionality in every class that required it would result in a
substantial amount of duplication. Furthermore, caching is not a core competency for most objects and is thus ripe for consolidation and reuse with AOP.
The caching sample application
comprises four files:
- CachingInterceptor.javaHouses the AOP code that performs the caching of method call results. This code is known as advice.
- User.javaA basic class with getter and setter methods for a "name" field. The CachingInterceptor AOP code is applied to this class.
- CacheTest.javaA sample
application that illustrates the CachingInterceptor AOP code in use.
- dynaop.bshThe BeanShell
script that specifies where the CachingInterceptor advice should
be applied. These specifications are known as pointcuts.
The following sections explain each of these files in detail.
The CachingInterceptor.java File. The CachingInterceptor class encapsulates the caching code that can be applied to any class in a standard, generic way. This class caches the methods' results after they have been called initially. The CachingInterceptor class then intercepts subsequent calls to methods whose results have been cached and returns them directly from the cache, providing fast responses for long-running methods. Listing 2 shows the CachingInterceptor class.
The CachingInterceptor class includes the intercept(), calculateCacheCode(), and getFullMethodName() methods. The intercept() method implements the dynaop.Interceptor interface and houses the core advice code for the aspect. This method is called before
an aspected method is invoked and is responsible for invoking the aspected method. Essentially, the intercept() method is a proxy that is used to inject functionality that executes before the method being proxied. Additionally, the intercept() method controls whether the proxied method actually gets invoked.
The intercept() method of the CachingInterceptor class calls the calculateCacheCode() method to calculate a cache key for the method being invoked. A cache lookup using the
key is then performed for the method being invokedin this case, the getName() method in the User class, which is described in the following sectionto see if its results have been cached. If they have, the cached results are returned and the User.getName() method is not invoked again. If they haven't, the User.getName() method
is invoked and its results are cached for subsequent calls. This is a basic caching implementation and does not account for expiring entries in the cache or for making sure the cache does not become too large.
The User.java File. The User class is a simple class with a single field including a getter and a setter method for the field. The CachingInterceptor aspect is applied to this class's getName() method. Listing 3 shows the User class.
Note that each method in this class has a System.out.println() call, which indicates when the method is called. These calls are used to illustrate exactly what is happening when the sample application is run.
The CacheTest.java File. The CacheTest class contains the code for a sample application that illustrates the CachingInterceptor aspect in use. The application simply instantiates a User object and calls its getName() method multiple times, showing that successive calls to the method result in cache accesses versus direct accesses to the method. Listing 4 shows the CacheTest class.
CacheTest contains a main() method, so it can run as a standalone application.
The dynaop framework uses a runtime-based weaving mechanism for injecting AOP code into objects, so directly instantiating the User object, using the new operator, does not
return an aspected object. To use the CachingInterceptor aspect in the sample application, the CacheTest class uses dynaop's ProxyFactory class to instantiate the User object. Once you instantiate it by using ProxyFactory.getInstance()
.extend(), you can use the User object as you normally would any other object. The execution of AOP code is transparent from this point.
The dynaop.bsh File. The dynaop.bsh file is
a BeanShell script used to specify the pointcuts where the CachingInterceptor advice is applied. Following are the
contents of the dynaop.bsh file:
// Apply interceptor to all
// getter methods.
interceptor
(
User.class,
GET_METHODS,
new CachingInterceptor()
);
This simple script specifies that the CachingInterceptor advice should be applied to all get methods of the User class. GET_METHODS is one of several constants dynaop uses for conveniently specifying groups of pointcuts. You can also explicitly specify individual methods for pointcuts if necessary.
Compile and Run the Application
Finally, compile and run the application. Assuming that you have placed the four files for the application in the directory where you installed dynaop, such as c:\java\dynaop,
the following command compiles
the application:
javac -classpath .\bsh-2.0b1.jar;
.\cglib-asm-1.0.jar;
.\dynaop-1.0-beta.jar;
.\jakarta-oro-2.0.7.jar
*.java
Once you have compiled the sample application, execute the following command to run it:
java -classpath .\;
.\bsh-2.0b1.jar;
.\cglib-asm-1.0.jar;
.\dynaop-1.0-beta.jar;
.\jakarta-oro-2.0.7.jar
CacheTest
Figure 1 shows the sample application's output. The application first calls the setName() method of the User class
and then calls the getName() method. Both of these calls are invoked on the User class; subsequent calls to getName() hit the cache and are not invoked on the User class.
|
| Figure 1: Command Prompt
|
Conclusion
AOP provides a rich new platform
for creating software that removes unnecessary responsibilities from classes and promotes extreme code reuse. Practical everyday uses for
AOP range from abstracting logging code to abstracting security and caching code, but AOP's applicability won't stop there. As AOP continues
to mature and evolve, there will be many more uses for it.
James Holmes (james@jamesholmes.com) is an independent Java consultant and committer on the Struts project. He is the author of Struts: The Complete Reference and coauthor of The Art of Java (both from McGraw-Hill/Osborne).
|