A Tour of AOP
Join the DZone community and get the full member experience.
Join For Free[img_assist|nid=4980|title=|desc=|link=none|align=right|width=150|height=188]The AOP methodology is just that—a methodology. In order to be of any use in the real world, it must be implemented, or realized. As with any methodology, it can be implemented in various ways. For example, one realization of the OOP methodology specification consists of the Java language and tools such as the compiler. In a similar manner, each realization of AOP involves specifying a language or a framework and associated tools. Like any other programming methodology, an AOP implementation consists of two parts:
- The language specification describes the language constructs and syntax to express implementation of the core and crosscutting concerns.
- The language implementation verifies the code’s adherence to the language specification and translates the code into an executable form. This is commonly accomplished by a compiler or a runtime agent
This article is based on chapter 1 from AspectJ in Action, Second Edition by Ramnivas Laddad. Courtesy of Manning Publications. All rights reserved.
The AOP language specification
Any implementation of AOP must specify a language to implement the individual concerns and a language to implement the rules for weaving the concern implementations together. While I talk in terms of two separate languages, an AOP system may not distinguish between the two parts. This is likely to be the case in future AOP languages. Let’s take a closer look at these two parts.
IMPLEMENTATION OF CONCERNS
As in other methodologies, the concerns of a system are implemented into modules that contain the data and behavior needed to provide their services. For example, a module that implements the core part of the caching concern will maintain a collection of cached objects, manage the validity of the cached objects, and ensure bounded memory consumption. To implement both the core and crosscutting concerns, we normally use standard languages such as C, C++, and Java.
WEAVING RULES SPECIFICATION
Weaving rules specify how to integrate the implemented concerns in order to form the final system. For example, once you implement the core part of the caching concern in a module (perhaps through a third-party class library), you need to introduce caching into the system. The weaving rule in this case specifies the data that needs to be
cached, the information that forms the key into the cache storage, and so forth. The system then uses these rules to obtain and update cache from the specified operations.
The power of AOP comes from the economical way of expressing the weaving rules. For instance, to modularize tracing concern in for listing 1, you can specify to log all the public operations in the system in just a few lines of code
Listing 1: Business logic implementation along with crosscutting concerns
public class SomeBusinessClass extends OtherBusinessClass {
... Core data members
... Log stream #5
... Cache update status #3
... Concurrency control lock #2
... Override methods in the base class
public void someOperation1(<operation parameters>) {
... Ensure authorization #1
... Lock the object to ensure thread-safety #2
... Ensure cache is up-to-date #3
... Start transaction #4
... Log the start of operation #5
... Perform the core operation
... Log the completion of operation #5
... Commit or rollback transaction #4
... Unlock the object #2
}
... More operations similar to above addressing multiple concerns
}
#1 Security check
#2 Concurrency control
#3 Caching
#4 Transaction management
#5 Tracing
For example, here is weaving specification for the tracing aspect.
- Rule 1: Create a logger object.
- Rule 2: Log the beginning of each public operation.
- Rule 3: Log the completion of each public operation.
This is much more succinct than actually modifying each public operation to add logging code. Since the tracing concern is modularized away from the class, it may focus only on the core concern as follows.
public class SomeBusinessClass extends OtherBusinessClass {
... Core data members
... Override methods in the base class
public void someOperation1(<operation parameters>) {
... Perform the core operation
}
... More operations similar to above
}
Compare this class with the one in listing 1.1: All the code to perform tracing—the ancillary concerns from the class’ point of view—have been removed and only the core business logic remains. As we will see in the next section, an AOP implementation will combine the classes and aspects to produce a woven executable.
Weaving rules can be very general or very specific in the ways they interact with the core modules. For example, in the previous logging example, the weaving rules did not need to mention any specific classes or methods in the system. On the other end of the spectrum, a weaving rule may specify that a business rule that is to be applied to only a specific methods, such as the credit and debit operations in an Account class or the ones that carry the @ReadOnly annotation. The specificity of the weaving rules determines the level of coupling between the aspect and core logic.
The language used for specifying weaving rules could be a natural extension of that language or something entirely different. For example, an AOP implementation using Java as the base language might introduce new extensions that blend well with the core Java language, or it could use a separate XML-based language to express weaving rules.
The AOP Implementation
The AOP implementation performs two logical steps: It first combines the individual concerns using the weaving rules, and then it converts the resulting information into executable code. AOP implementation, thus, requires the use of a processor—weaver—to perform these steps.
The weaver can be implemented in various ways. A simple way is through source-to-source translation. Here, the weaver processes source code for individual classes and aspects and produces woven source code. A regular language compiler may then process the formed code. The aspect compiler then feeds this converted code to the base language compiler to produce the final executable code. Using this approach, a Java-based AOP implementation converts individual source input files into woven Java source code and then lets the Java compiler convert it into the byte code (in fact, this implementation technique was used in early implementations of AspectJ). This simple approach suffers from several drawbacks as the executable code cannot be easily traced back to the source files written. For example, stack traces would indicate line numbers in woven source modules.
Another approach could be that the source code would first be compiled into class files using the base language compiler. The class files would then be fed to the aspect compiler, which would weave the aspects into the class files to produce woven class files. Figure 1 shows a schematic of a compiler-based AOP language implementation.
[img_assist|nid=4975|title=|desc=Figure 1 An AOP language implementation that provides a weaver in the form of a compiler. The compiler takes the implementation of the core and crosscutting concerns and weaves them together to form the final system.|link=none|align=undefined|width=600|height=303]
It may also be possible to push the weaving process close to execution of the system. For example, if the implementation of AOP is Java-based, a special class loader or a VM agent could perform the weaving. Such an implementation will first load the byte code for the aspects, weave them into the classes as they are being loaded, and supply those woven versions of the classes to the underlying virtual machine (VM).
Yet another implementation possibility is through use of automatically created proxies. In this case, each object that needs weaving may be wrapped inside a proxy. Such implementation typically works well in conjunction with another framework that controls creation of objects. In this way, the framework can wrap each created object in a proxy.
So far, we have looked at the mechanics of an AOP system. Now we take a look at the fundamental concepts behind AOP.
By now, it should be clear that AOP systems help in modularizing crosscutting concerns. However, so are many other solutions, such as direct byte code manipulation tools, for example, CGLIB or ASM, direct use of the proxy design pattern, or even meta-programming. How do you recognize AOP from all these other options? To find out, we need to distill core characteristics of AOP systems into a generic model. If a system fits that model, it is an AOP system; otherwise, it is not.
In order to implement a crosscutting concern, an AOP system may include many of the following concepts:
- Identifiable points in the execution of the system: Such points may include execution of methods, creation of objects, or throwing of an exception. Such identifiable points in the system are called join points. Note that join points are present in all systems even those that don’t use AOP, since join points are simply points during execution of a system. AOP merely gives identifies and categorizes these points.
- A construct for selecting join points: Implementing a crosscutting concern will require selecting a specific set of join points. For example, the tracing aspect discussed earlier needs to select only the public methods in the system. In AOP, the pointcut construct selects any join point that satisfies the criteria. This is similar to an SQL query selecting rows in database (we will compare AOP with databases in section 1.7.2). Pointcuts also collect context at the selected points. For example, a pointcut may collect method argument as context. The concept of join points and construct of pointcuts together form an AOP system’s join point model.
- A construct to alter program behavior: Once a pointcut selects join points, we need to augment those join points with additional or alternative behavior. For example, when implementing tracing, you need to log entry into the public methods. The advice construct in AOP provides a facility to do so. An advice adds behavior before, after, or around the selected join points. Around advice surrounds the join point execution and may execute it zero or more times. Advice is a form of dynamic crosscutting since it affects the execution of the system.
- A construct to alter static structure of the system: Sometimes, to implement dynamic crosscutting effectively, you need to alter the static structure of the system. For example, when implementing tracing, you may need to introduce the logger field into each traced class. The inter-type declaration constructs make such modifications possible. Furthermore, in some situations, you may need to detect certain conditions, typically the existence of particular join points, before an execution of the system. The weave-time declaration constructs allow such possibilities. Collectively, all these mechanisms are referred to as static crosscutting given their effect on the static structure, as opposed to dynamic behavior changes to the execution of the system.
- A module to express all crosscutting constructs: Since the end goal of AOP is to have a module that embeds crosscutting logic, you need a place to express that logic. The aspect construct provides such a place. An aspect will contain pointcuts, advice, and static crosscutting constructs. An aspect may be related to other aspects in similar way to a class relates to others. Furthermore, aspects are a part of the system and aspects use the system (for example classes in it) to get its work done.
Figure 2 shows all these players and their relationship to each other in an AOP system.
[img_assist|nid=4976|title=|desc=Figure 2: Generic model of AOP systems. Note that not every system will implement each part of the model.|link=none|align=undefined|width=601|height=365]
Each AOP system may choose a subset of the model. For example, Spring AOP does not implement weave-time declarations due to its emphasis on runtime nature. On the other hand, the join point model is so central to AOP that every AOP system must support it. Everything else revolves around the join point model.
When you encounter a solution that modularizes crosscutting concerns, try to map it onto the generic AOP model. If you can, then that solution is indeed an AOP system. Otherwise, it is an alternative approach for solving the problem of crosscutting concerns. In the next section, we look at some commonly known alternatives to AOP.
The problem AOP addresses isn’t new. The concerns of auditing, transaction management, security, and so on emerged as soon as we started implementing nontrivial software systems. Consequently, there are many competitive technologies to deal with the same problem: frameworks, code generation, design patterns, and dynamic languages. Let’s take a look at those alternatives. Note that, while I compare these techniques as alternative to AOP and (not surprisingly) show how AOP outshines each of them when it comes to dealing with crosscutting concerns, I do not mean that these techniques are useless. Each of these techniques is appropriate for a set of problems. In fact, AOP can work alongside these techniques quite well. Furthermore, in some cases, AOP can enhance their implementation.
Frameworks
Frameworks such as servlets and EJB offer specific solutions to a focused set of problems. For example, the servlet specification offers a framework to deal with requests made using the HTTP protocol. Given that each framework deals with a specific problem, it may also provide some solutions for dealing with common crosscutting concerns in that problem space. For example, the servlet framework provides basic support for intercepting HTTP requests.
Similarly, the EJB framework addresses a wide set of crosscutting concerns such as transaction management and security. In the EJB3 specification, it even provides limited support for interceptors, which, to an extent, match the goals of AOP. However, as we will see later, it falls short of being a complete solution.
Note that you may use AOP along with an underlying framework. In such an arrangement, the core framework deals with the target problem and lets aspects deal with crosscutting concerns. For example, the core Spring framework deals with dependency injection for configuration and enterprise service abstraction to isolate beans from the underlying infrastructure details, while employing AOP to deal with crosscutting concerns such as transaction management and security.
Framework approaches to crosscutting concerns often, but not always, boil down to either employing some form of code generation or implementing appropriate design patterns. Let’s examine the two in more details.
Code generation
Code generation techniques shift some responsibility of writing code from the programmer to the machine. Of course, programmers do have to write code for the generators themselves. These techniques represent powerful ways to deal with a wide range of problems and often are helpful in raising the level of abstraction. They can modularize crosscutting concerns by modifying the original code such as adding observer notifications or producing additional artifacts, such as automatic proxy classes, thus taking care of one of the drawbacks of a direct use of design patterns—manual modifications in many places.
A variation of code-generation works at the compiled code level. Instead of producing source code that needs to be compiled into machine code, the code generator directly produces machine code. For Java, the difference between source code-level generation and byte-code generation is rather small, given how directly source code maps to byte-code.
In Java 5, the annotation language feature allows attaching additional information to the program elements. Code generation techniques can take advantage of those annotations to produce additional artifacts (such as Java code or XML configuration files). Java 5 even provides a tool, the Annotation Processing Tool (APT), to simplify the process. However, APT forces understanding low-level details such as the syntax tree and that makes it difficult for a programmer to use it unless he acquires specific skills. It is no surprise that you hardly see many non-framework programmers using APT. AOP, on the other hand, can provide simpler solutions to process annotations, as we will see in rest of the book.
Many systems, most notably AspectJ, use byte-code manipulation as the underlying technique in implementing AOP. The difference is how it employs the technique as a part of the overall AOP model. First, it provides a much simpler programming model making it easier for a developer to create modularized crosscutting implementations without knowing low-level details such as the abstract syntax tree. It essentially provides a domain-specific language (DSL) targeted towards crosscutting concerns. The user is isolated from byte-code manipulation mechanisms as well, which is not for the faint of heart. Furthermore, by limiting power, it nudges developers towards writing better code. In short, while code-generation is capable of doing anything AspectJ can do (and a whole lot more), AspectJ brings a level of discipline that is essential to good software engineering when it comes to dealing with crosscutting concerns.
Design patterns
Design patterns provide well-understood solutions to recurring problems. Some of the recurring problems are due to crosscutting concerns. This should be a big surprise, since while AOP is only about 10 years old, the crosscutting problem existed for as long as we have been creating software systems. In this section, we take a comparative view of some of the design patterns—observer, chain of responsibility, decorator and proxy, as well as interceptor—that help with crosscutting concerns. You will see that there are quite a few similarities and in fact, you can view a few design patterns as a “poor man’s” AOP implementation.
OBSERVER
A classic technique in UI programming is to respond to events such as mouse move and key pressed. For example, you may show a dialog box when a key is pressed. Even on server-side enterprise applications, message-oriented architectures involve sending and responding to messages. The observer design pattern forms the basis for the event programming to decouple the event source (the subject) from the event responder (the observer). When a subject changes its state, it notifies all observers of the change by calling a method such as notify<ChangeType>(), passing it an event object that encapsulates the change.
The notification method iterates over all the observers and calls a method on each (in message-oriented systems, these details change a bit, but the overall scheme remains the same). The called method in the observer includes the logic appropriate to respond to the event. The key point of the observer pattern is that the subject knows nothing about which particular objects are observing it. The pattern also supports an arbitrary number of observers.
AOP's advice may superficially look like an event responder. However, there are some important differences. First, there is no explicit firing of “events” in AOP. In other words, you won't see invocations such as notify<ChangeType>() thus decoupling of all observer pattern logic from the subject class. Second, the context collected by pointcuts (equivalent to information carried by an “event” object) is a lot more flexible and powerful in AOP. Pointcuts can collect just the right amount of context needed for advice logic. With a typical event model, you end up passing "everything that you might possibly need".
CHAIN OF RESPONSIBILITY
The chain of responsibility (COR) pattern, as shown in figure 3, puts a chain of processing objects in front of a target object. Before or after invoking the command object, processing objects may perform additional work or interrupt the chain.
Successful use of the COR pattern has two prerequisite: there is only one (or a small number) of target methods and the associated framework already supports the pattern. For example, the filter implementation in the Servlet framework implements the COR pattern. It works well there because both prerequisites are met: it targets only one method—doService() and the filter management code is implemented as a part of the framework itself. In this setup, some coarse grained crosscutting concerns—ones that deal at HTTP request level—may be modularized into servlet filters. However, anything that needs to go beyond the doService() method, filter offers no solution.
[img_assist|nid=4977|title=|desc=Figure 3: Chain of responsibility as implemented by the servlet framework. The filter chain allows applying additional logic such as coarse-grained security around the doService() method.|link=none|align=undefined|width=599|height=234]
AOP works in similar ways, except it doesn’t have either of the prerequisites. Instead, each aspect simply deals with the problem head on by advising appropriate code.
DECORATOR AND PROXY
The decorator and proxy design pattern use a wrapper object that can perform some work before, after, or around invocation of the wrapped object or its representation. This additional work can be crosscutting in nature. For example, each method may perform a security check before the wrapped object’s method is invoked.
[img_assist|nid=4978|title=|desc=Figure 4: The decorator design pattern. The original object is wrapped in the decorator that presents the same interface as the decorated object. Each method passes through the decoration, which can implement functionality such as security and transaction|link=none|align=undefined|width=479|height=365]
A direct use of decorator and proxy design patterns for crosscutting concerns implementation require substantial effort. However, these patterns may be used as the underlying implementation technique as a part of an AOP system. The Spring Framework makes use of the proxy design pattern internally to avoid exposing the pattern to the users. This isn’t unlike the byte-code manipulation technique—cumbersome as a programming technique to deal with crosscutting concerns, but a perfectly fine underlying technology to implement AOP systems.
Another design pattern, interceptor, is often used along with the proxy design pattern. Let’s compare how it stands up against AOP for crosscutting concerns.
INTERCEPTOR
The interceptor pattern allows expressing crosscutting logic into an interceptor object. By attaching the interceptor to specific program elements, you can have the interceptor logic invoked around the intercepted elements. This pattern when used along with the proxy or decorator design pattern offers a reasonable solution for a wide range of crosscutting problems. For example, Java supports creation of dynamic proxies, which can be configured with an interceptor. Implementing the interceptor pattern generically and successfully requires a fair amount of machinery and thus is best left to a framework.
Let’s consider the newest implementation of the interceptor pattern in EJB3. The earlier versions of the EJB framework offered a solution for a specific set of crosscutting concerns: transaction management and role-based security, primarily. EJB3 offers a way to modularize user-specific crosscutting concerns through the interceptor approach.
public class TracingInterceptor {
private Logger logger = ...
@AroundInvoke
public Object trace(InvocationContext context) throws Exception {
logger.log("Entering " + context.getMethod().getName()
+ " in " + context.getBean().getClass().getName());
return context.proceed();
}
}
Then you can apply the interceptor to target classes and methods as shown in the following code snippet:
@Stateless
@Interceptors({TracingInterceptor.class})
public class InventoryManagementBean {
...
}
You can target specific methods by marking each such method with the @Interceptors annotation. On the other extreme, you can declare an interceptor as a default interceptor, which applies to all beans, except those that opt-out. EJB3’s implementation has a few limitations, such as an interceptor may be applied only to EJBs and not to ordinary types in the system, which may pose restrictions on certain usages. The programming model is also a bit complex and type-unsafe as access to the intercepted context (intercepted bean, method name, method arguments) are accessed through the InvocationContext object whose method returns Object and may require casting before using.
However, the real problem with the EJB interceptor design (and many other similar interceptor implementations) is the missing key abstraction of pointcuts. Instead of declaring classes and methods that need to be intercepted, classes and methods need to declare that they need to be intercepted, reducing it to a more macro-like usage. As a result, while the logic equivalent to AOP’s advice is modularized, the pointcut equivalent logic is spread in all intercepted types. Further, due to generic nature of join point context, the interceptor method may need complex logic to pluck arguments from the correct place.
Note that the Spring framework, in versions prior to 2.0, used a mechanism similar to InvocationContext thus suffering from programming complexities similar to EJB3 interceptors. However, Spring’s AOP always used a notion of pointcut to avoid the problem of spreading selection logic in multiple places. Furthermore, the AspectJ integration introduced in Spring 2.0 removes the need for InvocationContext-like logic and raises the pointcut implementation to a new level.
Dynamic Languages
Dynamic languages have recently gained popularity and rightly so. Especially when combined with frameworks based on the languages, they can provide powerful solutions for a set of problems. For example, Ruby when combined with Rails or Groovy when combined with Grails provide simpler solutions for certain kinds of web applications. Each dynamic language offers a meta-programming facility that allows modifying the structure and behavior of a program during its execution. The meta-programming facility may modularize the advice portion of a crosscutting concern. For example, you may modify an existing method’s implementation to wrap it with code that performs the crosscutting functionality before or after dispatching the original method.
While meta-programming is a fine tool for dealing with crosscutting concerns, there are a few considerations. First, such usage requires switching to a dynamic language. The static vs. dynamic languages “war” hasn’t been concluded yet, nor it will be concluded anytime soon. So you will have to make a considered choice here. Second, meta-programming may be too powerful a tool for many programmers and a more disciplined approach might be more appropriate. Third, due to its constrained expression power, it is probably easier to create AOP tooling that helps in understanding the crosscutting structure. Such tooling is difficult to imagine with general-purpose meta-programming facilities offered by dynamic languages.
This is a reason why Dean Wampler, a long-time AOP expert, started a project to bring AOP to Ruby through the Aquarium project (http://aquarium.rubyforge.org). It shows that, while AOP is popular in statically-typed languages, it has a role to play even in dynamically-typed languages. Interestingly, as seen from this project, it is relatively easy to build AOP capabilities on the top of core meta-programming support provided by the underlying language. By providing an aspect-focused DSL to express pointcuts, Aquarium provides a solution to modularize the pointcut portion of AOP in Ruby.
It is instructive to note that the father of AOP, Gregor Kiczales, wrote the “The Art of the Metaobject Protocol (The MIT Press, 1991)” book. He clearly knows meta-programming! He still thought that AOP is better suited for crosscutting concerns and a direct application of meta-programming.
In a way, statically typed languages use AOP on the way to more comprehensive meta-programming support in order to break free from the limitations posed by the language, whereas dynamic languages benefit from AOP as a “disciplined” application of meta-programming.
Opinions expressed by DZone contributors are their own.
Comments