Example 3: Characteristic-based implementation modularization
Operations with the same characteristics should typically implement common behaviors. For example, a wait cursor should be put before any slow method executes. Similarly, you may need to authenticate access to all security-critical data.
Since such concerns possess a crosscutting nature, AOP and AspectJ offer mechanisms to modularize them. Because a method's name might not indicate its characteristics, you need a different mechanism to capture such methods. To create such a mechanism, declare the aspect adding characteristic-based crosscutting behavior as an abstract aspect. In that aspect, declare an abstract pointcut for methods with characteristics under consideration. Finally, write an advice performing the required implementation.
Consider SlowMethodAspect
's implementation. It declares the abstract slowMethods()
pointcut and advises it to first put a wait cursor, proceed with the original operation, and finally restore the original cursor, as Figure 4 shows.
Here's the code:
// SlowMethodAspect.java import java.util.*; import java.awt.*; import java.awt.event.*; public abstract aspect SlowMethodAspect { abstract pointcut slowMethods(Component uiComp); void around(Component uiComp) : slowMethods(uiComp) { Cursor originalCursor = uiComp.getCursor(); Cursor waitCursor = Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR); uiComp.setCursor(waitCursor); try { proceed(uiComp); } finally { uiComp.setCursor(originalCursor); } } }
Two test components, GUIComp1
and GUIComp2
, nest a concrete implementation of the aspect. For example, GUIComp1
contains following aspect:
public static aspect SlowMethodsParticipant extends SlowMethodAspect { pointcut slowMethods(Component uiComp) : execution(void GUIComp1.performOperation1()) && this(uiComp); }
Whereas, GUIComp2
contains following aspect:
public static aspect SlowMethodsParticipant extends SlowMethodAspect { pointcut slowMethods(Component uiComp) : (execution(void GUIComp2.performOperation1()) || execution(void GUIComp2.performOperation2())) && this(uiComp); }
Compared to this article's other examples, in this example the aspected classes now no longer remain oblivious to other aspects; they include code to participate in the collaboration. However, such knowledge is limited -- it merely declares a method with certain known characteristics. Also note, you need not implement SlowMethodsParticipant
as a nested aspect. However, since these aspects completely depend on the surrounding classes, making them nested aspects helps keep the pointcut coordinated with implementation changes.
That usage pattern lets you provide aspectual class interfaces and lets you capture semantics- and characteristics-based pointcuts not otherwise possible by property-based pointcuts.
You'll find the complete code for both test components in GUIComp1.java
and GUIComp2.java
, and a simple test application in Test.java
(see Resources).
Example 4: Implement flexible access control
With AspectJ you can declare compile-time warnings and errors, a mechanism with which you can enforce static crosscutting concerns. For example, such a mechanism can enforce an access-control crosscutting concern. Java's access controls -- public
, private
, package
, and protected
-- do not offer enough control in many cases. Consider, for example, a typical Factory design-pattern implementation. Oftentimes, you want only the factory to create the product objects. In other words, classes other than the factory should be prohibited from accessing constructors of any product class. While marking constructors with package
access offers the best choice, that works only when the factory resides in the same package as the manufactured classes -- quite an inflexible solution.
C++'s friend mechanism controls access to a class from other specified classes and methods. Similarly, with AspectJ you can implement such functionality in Java, as well as a much more fine-grained and expressive access control.
The following code declares a simple Product
class with a constructor and a configure()
method. It also declares a nested FlagAccessViolation
aspect, which in turn declares one pointcut to detect constructor calls from classes other than ProductFactory
or its subclasses, and another pointcut to detect configure()
method calls from classes other than ProductConfigurator
or its subclasses. It finally declares either such violations as compile-time errors. ProductFactory
, ProductConfigurator
, or its subclasses could reside in any package, and both pointcuts could specify packages if necessary. You can further restrict access to, say, the createProduct()
method, using the withincode()
pointcut instead of within
:
// Product.java public class Product { public Product() { // constructor implementation } public void configure() { // configuration implementation } static aspect FlagAccessViolation { pointcut factoryAccessViolation() : call(Product.new(..)) && !within(ProductFactory+); pointcut configuratorAccessViolation() : call(* Product.configure(..)) && !within(ProductConfigurator+); declare error : factoryAccessViolation() || configuratorAccessViolation() : "Access control violation"; } }
The ProductFactory
class calls the Product.configure()
method, thus causing a compile-time error with a specified "Access control violation" message:
// ProductFactory.java public class ProductFactory { public Product createProduct() { return new Product(); } public void configureProduct(Product product) { product.configure(); } }
The rest in short
In this section, I outline a few simple, yet powerful AspectJ usages. You may implement these examples as part of your AspectJ learning process.
Logging and debugging
You can easily try a logging and debugging project in AspectJ. Using AspectJ for logging and debugging also lets you log different contextual information without touching the main code.
Design by contract
Design by contract (DBC) requires explicit contracts that must hold true at various execution points, such as before and after each operation. You can use AspectJ to enforce DBC by creating aspects containing advices to check contracts at required execution points. If you wish to not enforce contracts in a production environment, simply exclude DBC aspects from your production build.
Realize design patterns
AOP allows interesting twists when you implement certain design patterns. For example, AOP lets you look at MVC in a new way. Without AOP, MVC models are responsible for three concerns: state management, listener management, and state update notification. With AOP and AspectJ, you modularize each concern, which also lets you use models originally written only for state management, for example, Date
or Point2D
in MVC.
Lazy creation/initialization
It's common to optimize memory-intensive objects by lazily creating them. As a result, they exist just in time to serve for the first time. As with other optimizations, you'd use lazy instantiation because of profiling. However, it's invasive to change your implementation everywhere to lazily create certain fields. At the most, OOP only can require programmers to always use a get<Field>()
method and modularize logic for "create-if-needed" in that method. With that approach, if a programmer forgets to use this method and does a direct access, a crash awaits. Marking such fields with private access does not help for in-class field access. Even if you discover the need earlier during the design phase and the problem mentioned above is unimportant, it is nice to separate optimization concerns to avoid overloading the main implementation logic.
AspectJ's get()
pointcut lets you capture access to a field. You can advise such a pointcut to create an object if the field was null
. With this, the logic of checking for null
and creating an object if needed is modularized.
Caching
High performance applications need to cache expensive-to-create objects. For example, a server serving images, say stock charts, may need to cache byte-stream image representations. AspectJ can help separate caching concern implementations. With such a scheme, you capture image creation joinpoints, looking into the cache for an available image for some given data, then using the cached image instead. If no such image is available, the advice to such joinpoints creates an image and stores it in a hash table. The caching implementation could also employ a soft reference or some other scheme to ensure cached images do not cause the application to starve for memory. By using AspectJ, you can make noninvasive changes if the caching logic needs to change.
Parting thoughts
Be warned before reading this section as it represents my opinionated view of the world!
AOP and AspectJ solve real problems, and early adopters are increasingly trying AOP concepts with AspectJ. Like any other new programming paradigm, it will take time before both technologies reach their full potential. As more developers use AspectJ, the experience gained will guide the language's evolution. Simultaneously, design patterns and anti-patterns will appear. AOP's adaptation as a mainstream programming methodology will be a fun thing to watch and, better still, participate in.
I want to thank Kavita Laddad and Rick Warren for reviewing this series.
This story, "I want my AOP!, Part 3" was originally published by JavaWorld.