dcsimg

Zen and the Art of Aspect-Oriented Programming

As enterprise systems evolve from concept to code, an otherwise clean design can become downright, well, messy, as the practical plumbing of logging, caching, transactions, and more infiltrates modules. Wouldn't it be better if, say, logging were just another module? Enter aspect-oriented programming.

Consider the life cycle of a typical e-commerce project. First, you might decide to architect your code around an object-relational (OR) mapping tool, such as Java Data Objects or Hibernate, and J2EE services, such as servlets and the Java Authentication and Authorization Service. Next, you create interfaces and classes representing business concepts such as a customer, SKUs, a shopping cart, inventory, and payment authorization. At this point, your code is clean and comprehensible.

Then you encounter some inexplicable, intermittent behavior. “Of course! I need logging,” you say. So, you meticulously modify all of the methods in all of the classes to use log(). Now when a bug occurs, you examine the log and understand the sequence of activities that lead to the bug.

But before you get to pat your own back, you realize that performance isn’t up to snuff. You profile your code. “I need caching!” you exclaim. You modify many classes to introduce caching, all the while fully aware that you still need to implement a security solution, too. You implement that, but now your code is messy, and the clean outlines of your original design begin to blur.

This saga continues much the same way for error handling, transaction management, business rules, and so on. Even if you’d used a framework such as Enterprise Java Beans (EJB), the story wouldn’t be much different: the work would start off a little easier — until you have to venture beyond the boundaries of the framework.

It’d be nice if the situation were different. Need logging? Just write a new module that logs all method execution. Need caching? Add a module. Need security? Add another module, all without modifying the core implementation, leaving it as clean as it was during the initial phase.

Sound too good to be true? Not so. Aspect-oriented programming (AOP) can make this desired story a reality. Let’s look at AOP’s concepts and terminologies, learn how it works, and look at some example applications to better appreciate its benefits.

Crosscutting Concerns

In the typical e-commerce system, logging, caching, security, error handling, transaction management, concurrency control, and business rule implementation are crosscutting concerns, or requirements and design elements that affect multiple modules. Conventional implementations of crosscutting concerns cause two problems: tangling and scattering. Tangling occurs when a program unit (such as a class or a method) implements multiple concerns in an intertwined fashion. Scattering occurs when multiple modules include code to implement a single concern. The ultimate affect of tangling and scattering is lowered productivity, hard-to-understand code, and poor code quality due to ambiguities in design and inconsistencies in implementation.

Crosscutting concerns also affect virtually every part of the development process.

* In the design phase, they present the “architect’s dilemma” of whether to under-design or over-design. Under-design is motivated by the difficulty of predicting future features. “You ain’t gonna need it,” as coined by Kent Beck, so why pay an upfront cost? However, if you do need crosscutting features later, it may cause system-wide changes. Over-design, on the other hand, favors a design that accommodates possible future requirements.

* In the implementation phase, crosscutting concerns cause obscure code, unfocused effort, and poor mapping between design intentions and code.

* Finally, during the maintenance and evolution phases, the same concerns cause system-wide changes and risk inadvertent modification to system behavior.

These crosscutting concerns are common in enterprise systems. Just consider the last logging or error handling policy you implemented.

AOP to the Rescue

AOP is a new programming methodology that captures and implements crosscutting concerns as modules. Where object-oriented programming (OOP) encapsulates core concerns into a class, AOP encapsulates crosscutting concerns into aspects. An AOP system allows you to implement crosscutting concerns and express weaving, or composition, rules. Another AOP system component, the weaving processing model, realizes the implementation.

To implement individual concerns, an AOP system uses an existing programming language such as Java, C++, or Ruby. To express weaving rules, an AOP system may use an extension of the base programming language or may specify a completely new language. For example, AspectJ, AspectC++, and AspectR extend each base language (Java, C++, and Ruby, respectively) to specify weaving rules.

There are three processing models for AOP systems, classified by when they execute: at build time, load time, or runtime.

* BUILD TIME. In this technique, weaving takes place when building a system. One build time approach is to compile the sources using a special compiler provided by the AOP system. Since the compiler itself performs most of the weaving, the resulting systems have good load-time/runtime performance. Further, in this model, weave-time declarations (such as declare warning and declare error in AspectJ) report warnings and errors during compilation when it is most helpful. Another build time option found in some AOP systems is to use the aspect compiler to weave aspects into sources that were previously compiled using a standard compiler. With this option, you can keep an existing build mostly intact and add a simple weaving step towards the end. This option is also useful for plugging in non-core aspects, for example, for tracing, profiling, or even caching.

* LOAD TIME. Weaving at load time takes places while the classes are loaded into the virtual machine (VM) using a custom class loader that transforms the loaded classes by weaving in aspects. This processing model allows developers or administrators to add new crosscutting behavior by modifying the load configuration and restarting the application.

In a container-managed environment, the system may support incorporating new aspects whenever the classes are loaded following their normal life cycle. For example, a web container could weave in new aspects every time it loads a servlet.

* RUNTIME (OR DYNAMIC). Extending the idea of load time weaving, the runtime processing model allows weaving at any time while the system is running. Such a processing model allows deploying and removing aspects dynamically in a running system.

When used with due caution, the runtime technique enables debugging and monitoring a system without restarting the application or forcing a reload of the affected classes.

Taking A Closer Look

Let’s review the concepts and terminologies of AOP. Once you understand AOP concepts in general, you can make a correspondence between the same programming elements in different AOP systems. (In this article, we’ll use AspectJ code snippets to show how each concept is mapped onto programming elements in that system.)

Figure One shows the relationship between various elements in an AOP system. The depicted model is a platform independent model (PIM) as used in model-driven architecture (MDA). Concrete AOP implementations such as AspectJ, AspectWerkz, and JBoss/AOP map some or all of the entities into programming constructs.








aop_02
Figure One: A conceptual model of an AOP system join point

For example, AspectJ maps the aspect into a class-like language element; JBoss/AOP maps the same to an XML element; whereas AspectWerkz maps it to a class with special annotations or an XML element.

* JOIN POINTS. A Join point is a well-defined point in the execution of a system. Every AOP system defines a subset of exposed join points available for crosscutting. Commonly exposed join points include method call, object creation, method execution, and exception handler execution. Join points that are unlikely to be exposed include conditional checks and looping constructs. An AOP system limits the exposed join points to promote robust aspects.

To understand the join points exposed by AspectJ, consider the following code snippet:


public class BankingFacade
{
private AccountDAO dao;

public void transfer(int fromId,
int toId, double amount)
{
Account to = dao.findAccountById(toId);
Account from =
dao.findAccountById(fromId);

from.transfer(to, amount);
}
}

When executing transfer(), the join points traversed correspond to the execution of that function: access to the dao field; the call to the findAccountById() method; a second access to the dao field; a second call to find AccountById(); and the call to the transfer() method on from. Using constructs described later, you can capture desired join points and implement crosscutting concerns to affect them.

* POINTCUTS. A pointcut is a program element that selects join points and extracts context. A pointcut selects join points based on their characteristics, such as class names, method names, argument types, and exception specification, and can extract context including the target object and method arguments, among others.

Figure One illustrates that a pointcut is a crosscutting element that captures join points. All the crosscutting elements, including pointcuts, use pointcuts. A pointcut may use other pointcuts for composition purposes.

Let’s see how you can use a pointcut in AspectJ to capture join points. To capture the execution of the transfer() method, you can use:


execution(public void BankingFacade.
transfer(Account, double, Context))

Since pointcuts show their power in economically expressing a set of join points, AspectJ allows use of three wildcards: * matches any number of any characters except .; .. matches any number of characters including .; and + matches a class or any of its subclasses.

For example, to capture execution of all methods in classes whose names end in BankingFacade and their subclasses, taking any number and types of arguments, you can use the execution(* *BankingFacade+.*(..)) pointcut.

Pointcuts can be named or can be anonymous. An anonymous pointcut is defined at the point of its use, whereas a named pointcut is defined separately and is used later.

Here is an example of a named pointcut that captures all method executions of the Account class:


public pointcut bankingExecs():
execution(* BankingFacade.*(..))

The bankingExecs() pointcut now may be used as a part of another pointcut definition, advice, or a static crosscutting construct, as you’ll see shortly.

* ADVICE. Advice is extra code to be executed upon reaching selected join points. In spite of the name, advice always executes (it’s like the advice your boss gives you). An AOP system ensures that advice runs whenever a join point is encountered that matches the pointcut for that advice.

Typically, advice comes in three flavors: before, after, and around. A before advice executes before the join point’s execution, whereas an after advice executes after the join point’s execution. An after advice may allow discrimination based on whether the join point executed successfully or failed with an exception.

An around advice surrounds the join point execution and takes control. It may not execute the captured join point, it may execute it with the original context or change the context (such as parameters), it may not execute it at all, or even execute multiple times.

Figure One shows that an advice is a dynamic crosscutting element — it has an effect at runtime. It uses a pointcut to specify applicable join points.

Let’s consider how you’d advise the bankingExecs() pointcut defined earlier to log the method name before executing a matching method. We can use a before advice as follows:


before() : bankingExecs()
{
logger.info(“Entering: “+
thisJoinPointStaticPart.get
Signature().toShortString());
}

Using this advice, a message is logged before executing any methods in the BankingFacade class.

You might be wondering about the thisJoinPoint StaticPart variable. Just as each instance method in Java has access to the special this variable, each advice has access to additional special variables. thisJoinPoint carries dynamic information about the current join point, including the current object, the target of a call, and the arguments that were passed. thisJoinPointStaticPart carries a static subset of this information, such as the method currently executing.

* INTER-TYPE DECLARATIONS. Inter-type declarations (also known as introductions) enable modifications to types in the system, such as introducing new methods and fields to a type, declaring a new parent type for an existing type, and modifying the exception specification of a method. Figure One shows that an inter-type declaration is a static crosscutting element and uses pointcuts to express declaration specifics.

The following snippet makes all of the types in org.sample.banking and its subpackages implement the Loggable interface…


declare parents: org.sample.banking..*
implements Loggable;

… and the code immediately below shows yet another example of static crosscutting, one that softens an exception by converting a checked exception into an unchecked one.


declare soft: SQLException: execution(*
org.sample.banking.model..*(..))

The snippet softens a SQLException thrown at any execution join point defined by any class in the org.sample. banking.model package and its subpackages.

* ENFORCEMENT DECLARATIONS. Enforcement declarations allow for custom compiler warnings and errors based on crosscutting rules. (Errors are emitted at link or load time for AOP systems that weave in those phases). A common use of enforcement declarations to implement system policies such as signaling improper dependencies between layers.

For example, the following declaration causes a compiler to produce a warning if there is any call to a Logger.debug() method in the system:


declare warning: call(* Logger.debug(..)):
“possible tracing call”;

As shown in Figure One, an enforcement declaration is a static crosscutting element. It uses pointcuts to capture statically-determinable conditions.

* ASPECTS. The aspect is the unit of crosscutting modularity, and it holds any of the crosscutting elements. Again, as shown in Figure One, an aspect itself is a crosscutting element and is composed of other crosscutting elements. An aspect may be related to another aspect by inheritance relationship (to allow reuse), or an aspect may be related with other aspects by a precedence relationship, which ensures a deterministic behavior when multiple aspects affect the same parts of the system. Aspects are instantiated automatically by the AOP system.

Here is an aspect that enables logging in the system by combining a few crosscutting elements discussed earlier:


aspect BankingLoggingAspect
{
public pointcut bankingExecs() :
execution(* BankingFacade.*(..));

before() : bankingExecs()
{
logger.info(“Entering: ” +
thisJoinPointStaticPart.get
Signature().toShortString());
}
}

AOP in Enterprise Systems

Let’s dig in to how AOP can be used in practice, looking at how AOP can improve upon traditional approaches for tracing, error handling, transaction management, and buffering.

Listing One shows a method in an BankingFacade class that transfers funds among bank accounts. It runs in a J2EE container, and it uses the Data Access Object (DAO) pattern to separate data access from business logic. The class is an example of the Facade pattern that aggregates business functions into a set of services.




Listing One: An BankingFacade business object


public class BankingFacade
{
private static Logger logger =
Logger.getLogger(BankingFacade.class);
private AccountDAO dao;

public void transfer(int fromId, int toId,
double amount)
{
if (logger.isDebugEnabled()) {
logger.debug(“Entering transfer from
“+fromId+” to “+
toId+” of “+amount);
}
try {
dao.startTransaction();
Account to = dao.findAccountById(toId);
Account from =
dao.findAccountById(fromId);
from.transfer(to, amount);
dao.update(from);
dao.update(to);
dao.commit();
} catch (SQLException se) {
ModelException me = new
ModelException(“database exception”, se);
logger.error(me.getMessage(), me);
dao.rollback();
throw me;
} catch (EntityNotFoundException e) {
logger.error(e.getMessage(), e);
dao.rollback();
throw e;
} finally {
if (logger.isDebugEnabled()) {
logger.debug(“Exiting transfer”);
}
}

}

Naturally, many other methods in the BankingFacade class and in other business objects have very similar boilerplate code to trace method entry and exit, to catch and convert exceptions, and to manage transactions. Let’s look at how AspectJ can improve on this situation.

The first step is to pull tracing out from the business methods into an aspect.

To accomplish this, use the pre-built ajee aspect library from the aTrack project (https://atrack.dev.java.net). aTrack is an open source project that includes a standard library of aspects and an example bug tracking application.

To use the aspect, refactor in two steps. First, extend the library aspect to trace methods in the banking application, as shown in Listing Two.The ajee library includes an abstract aspect ajee.tracing.ExecutionTracer that defines how to trace the execution of any module. The aspect is abstract in much the same way a class can be: it needs to be extended with a concrete aspect that fills in missing details. In this case, there is an abstract pointcut that defines the scope of application of the aspect — what method executions should be traced?




Listing Two: Tracing with a library aspect


public aspect BankingExecutionTracing extends ExecutionTracer
{
public pointcut scope():
within(org.sample.banking..*); // |1|
declare parents: org.sample.banking..*
implements Loggable; // |2|
}

The concrete definition of scope |1| uses the within pointcut descriptor to pick out any join points defined in the org.sample.banking package or any of its subpackages. This pointcut is then used to apply before and after advice in ExecutionTracer to trace method execution join points.

The tracing facility cooperates with a log management aspect that tracks and provides access to the class-level logger for any class that implements the Loggable marker interface. In |2|, the tracing aspect uses declare parents to make all the classes in the org.sample.banking package or any of its subpackages implement Loggable, so they can be traced.

By adding this four line aspect, the system now has a systematic tracing facility that is enabled by configuring our class-level logger to a DEBUG level (say, in log4j.properties).








aspects_02
Figure Two: Tracing with a library aspect

(After removing the hand-written tracing code) the output is illustrated in Figure Two. It’s not necessary to understand how the ExecutionTracer module works, any more than it’s important to understand how a database driver interfaces with a database. With reusable aspect libraries, you can apply prebuilt, pretested, and preoptimized tracing behavior to your code.

With the addition of the aspect, the tracing is now consistent and accurate. As an added bonus, the library provides more complete and more flexible information. It outputs the arguments and return values from any method, and it provides systematic support for changing the strings produced when tracing a given type.

The BankingFacade object is now simplified, as shown in Listing Three. The calls to Logger have been removed, there’s no need to create and initialize a Logger field (the log management aspect does that), and the core method is easier to read.




Listing Three: BankingFacade core logic after extracting tracing


public class BankingFacade
{
private AccountDAO dao;

public void transfer(int fromId, int toId,
double amount)
{
try {
dao.startTransaction();
Account to = dao.findAccountById(toId);
Account from =
dao.findAccountById(fromId);
from.transfer(to, amount);
dao.update(from);
dao.update(to);
dao.commit();
} catch (SQLException se) {
ModelException me = new ModelException
(“database exception”, se);
logger.error(me.getMessage(), me);
dao.rollback();
throw me;
} catch (EntityNotFoundException e) {
logger.error(e.getMessage(), e);
dao.rollback();
throw e;
} …
}

While the modularization of tracing represents a significant improvement, error handling and transaction management are still tangled in with core logic and represent more boilerplate code that could be consolidated as aspects.

Detecting and responding to errors throughout an application effectively is a challenging problem and solving it requires a comprehensive policy and a consistent application thereof.

For example, the transfer() method requires the application of three common policy elements:

1. Log context information about exceptions when they’re first determined to be a problem.

2. Convert exceptions that reflect implementation details into meaningful application exceptions when exiting an application layer (while preserving original exception information).

3. Respond to exceptions at well-defined points — in this case to clean up resources.

Listing Four shows an aspect that implements these policies for all of the banking application’s domain tier. In |1|, the modelExecs pointcut picks out the execution of any method in any class contained in the org.sample.banking.model package or any of its subpackages. This is used by the advice at |2| to handle exceptions thrown by any of these methods. The advice executes whenever one of these join points terminates unexpectedly by throwing a SQLException. This advice calls a private helper method in the aspect, handle Exception(), to handle the exception at |3|. It passes the special variable, thisJoinPoint, which provides programmatic access to information about the current join point. The aspect has similar advice to handle NamingExceptions at |4|.




Listing Four: An error handling aspect


public aspect ModelErrorHandlingPolicy {
pointcut modelExecs() :
execution(* org.sample.banking.model..*(..));
// |1|

after() throwing (SQLException e) :
modelExecs() { // |2|
handleException(e, thisJoinPoint); // |3|
}
after() throwing (NamingException e) :
modelExecs() { // |4|
handleException(e, thisJoinPoint);
}

private void handleException(Exception e,
JoinPoint joinPoint) { // |5|
Object[] args = joinPoint.getArgs(); // |6|
String argStr = “args = [";
for (int i=0; i<args.length; i++) {
argStr += (i+" = "+args[i]);
}
String methodName =
joinPoint.getSignature().getName();
getLogger().error(e.getMessage()+” at
“+methodName+” for “+
joinPoint.getThis()+argStr+”]”, e);

throw new ModelException(e); // |7|
}

declare soft: SQLException: modelExecs();; // |8|
declare soft: NamingException: modelExecs();
}

For both exception types, handleException() at |5| captures a detailed error log by using the join point information. The log describes the object, method, and arguments in use when the exception occurred.

Exceptions in the model layer need to force any open transactions to rollback, which is accomplished at |7|. This code interacts with another aspect, TransactionManagement, that holds on to the current database connection and manages the DAO objects. (The complete transaction management aspect is omitted here, but it is available in the sample code online.)

Finally, the error handling method wraps the original exception with a new exception, ModelException, that’s meaningful for callers of model methods and throws it at |7|. The ModelException is an unchecked exception, so you don’t have to declare it in throws clauses throughout the model layer. However, SQLException and NamingException are checked exceptions that must be declared in our method signatures or caught. The code uses Aspect’s declare soft declarations at |8| to make the AspectJ compiler recognize that these exceptions are being converted to runtime exceptions and are not thrown out of any model methods. Without that advice, the declare soft form would also convert the named exceptions to a special Soft Exception, but here, the advice bodies catch and convert them first (they take precedence).

You may be wondering how exceptions cause transactions to be rolled back. This behavior is enforced by an another aspect, TransactionManagement, that holds on to the current database connection and manages the DAO objects. (Again, the complete transaction management aspect is omitted here, but it is available in the sample code online.)

Whether and where to use checked or unchecked exceptions is a religious debate among Java developers. This code shows an example of using unchecked exceptions. It’s also possible to write an error handling policy in AspectJ that uses checked exceptions.

After refactoring to extract transaction management and database access into an aspect as well, the final transfer() method is reduced to just the core elements of business logic, as illustrated in Listing Five.




Listing Five: Simplified business logic after refactoring


public void transfer(int fromId, int toId,
double amount)
{
Account to = dao.findAccountById(toId);
Account from = dao.findAccountById(fromId);
from.transfer(to, amount);
}

Perspectives on Aspects

Modularizing the crosscutting concerns of tracing, error handling, and transaction management has made the code easier to read and easier to test. A great deal of boilerplate code has been removed, replaced with a few aspects. Better yet, the crosscutting concerns are consistent and localized, making maintenance and modification cheaper. In addition to the examples uses shown here, AOP could also be used to extend the business objects to enforce fine-grained security and to publish events on a message queue whenever transactions occur.

So how can you use take advantage of this and start using AOP? The leading AOP implementations are all designed to make it easy to apply AOP using familiar languages and tools (especially for Java developers). As with any powerful new technology, it is important to plan adoption carefully and follow an effective learning curve. Start simply, perhaps using AOP only for development support and move gradually to using aspects for auxiliary features, infrastructure, and finally, business logic. This makes AOP easy and safe to start using, while scaling smoothly to give increasing benefits with increasing expertise.




Refactoring with Aspects

To refactor code to remove manual tracing, you can use a static crosscutting facility in AspectJ: declare warning. To find any code that calls debug() on a Logger, simply add temporary code to the tracing aspect:

declare warning: call(* debug(..)) &&
target(Logger) &&

scope(): “possible tracing call”;

When compiled, this code emits a list of warnings showing where debug() is called, many of which do tracing. This makes it easy to look at each location to remove scattered and tangled tracing from the code (especially with an IDE).

It is also possible to do the refactoring to use aspects in stages: first convert tracing in part of the code base and then expand the scope pointcut to cover more code in subsequent steps.




Ron Bodkin is the founder of New Aspects of Software, which provides consulting and training on application development with an emphasis on aspect-oriented programming and security. You can reach Ron at rbodkin@newaspects.com. Ramnivas Laddad is the author of AspectJ in Action (Manning, 2003). You can reach Ramnivas at ramnivas@yahoo.com.

Comments are closed.