devxlogo

Using Annotations with Aspects in Pre-Java 5 Versions

Using Annotations with Aspects in Pre-Java 5 Versions

spect oriented programming’s (AOP’s) rapid rise in popularity inspires both fear and worship?and for good reasons?it’s a very powerful software development technique that separates business logic from the infrastructure services that support that logic. In other words, AOP facilitates the centralization of concerns that cut across code boundaries, such as transactions, security, and auditing. Another compelling use of AOP is in leveraging aspects that enforce policies, such as programmatic security checks, rather than simply relying on programmers to adhere to coding standards.

But because AOP is such a powerful tool you must use it carefully?and for the right reasons. Aspects, by their very nature, can alter the behavior of almost any point of execution within your application. Furthermore, because aspects remove cross-cutting concerns from the normal flow of business logic, their insertion into the execution path of your application is invisible when looking at source code. One of the trickiest parts of working with aspects is defining how they should be woven into the flow of your application, a process known as defining a pointcut. Defining pointcuts is challenging because it’s difficult to select specific points in the flow of the program (join points) accurately using an expression. For more information about AOP and pointcuts, see the Related Resources in the left column of this article.

Annotations were introduced to the Java language with JSR 175 and the release of Java 5. They provide metadata about packages, classes, fields, and methods that tools and applications can use. Though interesting by themselves, annotations really shine when combined with the power of AOP, as they can alleviate some of the drawbacks of aspects. For example, annotations used in a pointcut can provide visibility into which methods are receiving additional behavior (advice) from your aspects. This lends greater precision and simplifies the pointcut definition itself. Annotations can provide not only a marker for pointcut selection, but also additional information to be used by the aspect when applying its advice.

The most popular example of this practice is declarative transaction management, in which you can use annotations to mark the methods that should be wrapped in a transaction and specify what the transaction isolation level and propagation should be. In fact, this illustrates the approach taken by many lightweight application containers today, and is one method used in EJB 3.0 to declare CMT beans.

What You Need

Pre-Java 5 Metadata
All the advantages sound good, but what if you’re stuck using an older version of Java that doesn’t support JSR 175 annotations? Fear not, for there are several options available to you. Most pre-Java 5 metadata solutions involve placing additional data within javadoc comments prefixed with the ampersand (@) character. Sun actually initiated this approach with their decision to define method deprecation within javadoc comments. Code generation tools such as XDoclet use the same approach?letting you process sources and create various other artifacts from the metadata contained in special javadoc comments.

Despite the power offered by metadata frameworks, all are still plagued by the unfortunate fact that data affecting the behavior of your code is expressed in comments.

Several frameworks for dealing with javadoc metadata have surfaced recently that give you almost all of the power offered by JSR 175 annotations. QDox and XJavaDoc both allow programs to read javadoc comments from source files. Jakarta Commons-Attributes lets you define annotations in a type-safe manner, scoped only to elements where they have meaning, and inherited automatically in subclasses. Also, you can use Backport 175 to create annotations in Java 1.3/1.4 that are bytecode compatible with JSR 175 annotations.

Spring uses Jakarta Commons-Attributes to enable the use of javadoc metadata by aspects. AspectWerkz and JBoss AOP both provide custom solutions with similar support. Backport 175 and load-time weaving let you integrate pre-Java 5 annotations with AspectJ. Despite the power offered by frameworks such as these, all are still plagued by the unfortunate fact that data affecting the behavior of your code is expressed in comments.

Although their implementations differ, these metadata frameworks share fundamental similarities. They all offer Ant tasks (AspectWerkz and Commons-Attributes tout Maven plugins as well) to process source code javadoc comments and to store metadata for retrieval at runtime. Typically the metadata is read using tools such as QDox or XJavaDoc, and then stored for later retrieval in XML, generated classes, or existing classes enhanced by tools such as ASM or BCEL. An API specific to each provider makes this information available at runtime.

Evaluating AOP Frameworks
If you are already using an AOP framework and would like to enhance your aspects using annotations, then you will most likely choose to stay with your current framework. However, if you are not yet using an AOP framework, then support for pre-Java 5 metadata should be a part of your evaluation criteria. A full evaluation of the many existing AOP frameworks is beyond the scope of this article; for a good comparison of AOP frameworks, consult the Related Resources in the left column of this article. My evaluation here focuses specifically on support for javadoc metadata in the most popular AOP frameworks.

When it comes to evaluating support for javadoc metadata, several factors merit consideration:

  • Type Safe Annotations?Type safe annotations are typically defined as an interface or JavaBean and expose allowable attributes as properties and constructor arguments.
  • Untyped Annotations?Some frameworks allow you to access javadoc metadata without having to pre-define your annotations.
  • Named Parameters?An annotation configuration that uses name-value pairs.
  • Anonymous Parameters?An annotation configuration that uses single values or a comma-separated list.
  • Target Restriction?The process of restricting annotations to elements of a particular type such as classes, methods, or fields.
  • Annotation Inheritance?A term meaning that annotations defined on superclasses are available on subclasses.
  • Annotation Storage?A scheme for storing metadata so that it can be made available at runtime.
  • Ant Task?Determine whether the framework provides an Ant task to process javadoc metadata.
  • Maven Plugin?Determine whether the framework provides a Maven plugin to process Java javadoc metadata.

Table 1 shows a feature comparison of four popular AOP frameworks (AspectJ, AspectWerkz, Spring AOP, and JBoss AOP) support for javadoc metadata.

Table 1. Metadata Framework Comparison: Because metadata support prior to Java 5 is not standardized, each framework supports it differently.

FeatureAspectJAspectWerkzSpring AOPJBoss AOP
Typed AnnotationsJSR 175 Only (*)YesYesYes
Untyped AnnotationsNoYesNoUnknown
Named ParametersJSR 175 Only (*)YesYesYes
Anonymous ParametersJSR 175 Only (*)YesYesUnknown
Target RestrictionJSR 175 Only (*)NoYesNo
Annotation InheritanceJSR 175 Only (*)NoYesUnknown
Annotation StorageNativeEnhanced ClassesGenerated ClassesEnhanced Classes or XML
Ant TaskYesYesYesYes
Maven PluginYes (Maven 1)Yes (Maven 1)Yes (Maven 1)No

* JSR 175 bytecode compatible annotations can be created using Backport 175. However you will need to use load-time weaving of aspects to prevent the Backport 175 enhanced classes from being overwritten.

Example Application?Ticket Broker

 
Figure 1. Ticket Broker Sequence Diagram: Both the seat reservation and billing must be completed as an atomic unit of work.

As an example I’ll walk you through the process of building a ticket broker application, which allows customers to purchase tickets to events. As Figure 1 illustrates, each order involves two steps: reserving the specified number of seats, and debiting the customer’s account for the purchase. These steps should be executed within the context of a transaction. In other words, they should either both succeed or neither should happen at all.

To follow along, download the companion source code for this article, which includes two versions of the Ticket Broker application. The first, named “initial,” contains a simpler initial version of the application in which transactions are not properly applied?a good version with which to start. The second, “final” version contains the completed application. See the readme.txt file included in the download for instructions on building and running the example application.

Author’s Note: The example application uses the Ant Tasks for Maven 2 to manage dependencies. Maven 2 is a powerful tool to encapsulate oft-repeated build tasks. But this new version is still in beta and doesn’t yet have robust plugin support. Using the Ant Tasks allows me to take advantage of Maven 2 dependency management while still being freed to use Ant with the AspectWerkz compilers. For more information on Maven 2 see my previous article.

Here’s the code for the purchaseTicket operation.

   ...   public void purchaseTicket(int customerAccountID,       int eventID, int quantity)   {      reserveSeats(eventID, quantity);         chargeCustomerAccount(customerAccountID, eventID, quantity);   }      private void reserveSeats(int eventID, int quantity)   {      Event event = eventDAO.findById(eventID);         if (quantity > event.getAvailableSeats())      {         throw new RuntimeException("Not enough seats available");      }         event.setAvailableSeats(event.getAvailableSeats() - quantity);         eventDAO.save(event);   }      private void chargeCustomerAccount(int customerAccountID,       int eventID, int quantity)   {      CustomerAccount account = customerAccountDAO.findById(         customerAccountID);      Event event = eventDAO.findById(eventID);         double amount = event.getTicketPrice() * quantity;         if (amount > account.getBalance())      {         throw new RuntimeException("Not enough money in account");      }         account.setBalance(account.getBalance() - amount);         customerAccountDAO.save(account);   }   ...

As you can see, this operation is composed of two steps. First, it attempts to reserve the seats requested by the customer. If the reservation succeeds, the operation then proceeds to charge the customer’s account by calling the chargeCustomerAccount method. If there are not enough seats, or if a customer’s account contains insufficient funds, the purchaseTicket method throws an exception. But if the chargeCustomerAccount method throws an exception then the system needs to roll back the seat reservation made in the first step.

A unit test verifies that the transaction semantics have not yet been properly applied on the purchaseTicket operation. This test provides confirmation that if a customer attempts to purchase more tickets than he can afford, the seat reservation is rolled back and his account balance remains unmodified. I have chosen to use DbUnit to populate the database with test data. The test data is shown below:

   src/test/resources/test.db.xml                                    

The test data contains three events and three customers. In the following scenario, the test case attempts to reserve two tickets for customer three to event three?the Norah Jones event. But as tickets to that event cost $100.00, and the customer account contains only $25.00, the customer cannot afford the tickets; in other words, the entire operation should be rolled back. Here’s the test code.

   ...   public void testPurchaseTicketInsufficientFunds()    throws SecurityException, NoSuchMethodException   {      try      {         // Parameters are (customerId, eventId, quantity)         service.purchaseTicket(3, 3, 2);            fail("Exception not thrown when customer has " +             "insufficient funds");      }      catch (Throwable e)      {         // Exception was thrown as expected      }         CustomerAccount customerAccount =          customerAccountDAO.findById(3);      assertEquals("Incorrect customer account balance", 25,          customerAccount.getBalance(), 0);         Event event = eventDAO.findById(3);      assertEquals("Incorrect number of seats left", 250,          event.getAvailableSeats());   }   ...

Execution of this unit test fails because the purchaseTicket method fails to roll back the seat reservation when the chargeCustomerAccount is unable to charge the customer due to an InsufficientFundsException. Here’s the test output:

      test:       [junit] Running          com.devx.ticketbroker.dao.impl.CustomerAccountDAOTest       [junit] Tests run: 4, Failures: 0, Errors: 0, Time elapsed: 0.688 sec       [junit] Running com.devx.ticketbroker.dao.impl.EventDAOTest       [junit] Tests run: 4, Failures: 0, Errors: 0, Time elapsed: 0.593 sec       [junit] Running com.devx.ticketbroker.service.impl.TicketServiceTest       [junit] Tests run: 2, Failures: 1, Errors: 0, Time elapsed: 0.469 sec      BUILD FAILED      ---   Excerpt from target/junit-reports/TEST-com.devx.ticketbroker.service.impl.TicketServiceTest.txt   ---      Testcase: testPurchaseTicketInsufficientFunds took 0.031 sec      FAILED   Incorrect number of seats left expected:<250> but was:<248> 

Defining a Transaction Annotation
To solve the problem, you can apply a transaction aspect using AspectWerkz 2.0. First, you must create an interface that serves to denote methods as transactional. This interface will also define a propagation type. While you could specify a particular propagation type (such as mandatory, requires new, etc.), for this example the mere presence of the transaction annotation will mark a method as transactional; thus, this attribute is purely for demonstration.

   package com.devx.ticketbroker.annotation;      public interface Transaction   {      public String propagationType();   }

Next, declare that the purchaseTicket method requires a transaction by adding the following javadoc comment to the TicketService.

   ...   /**   * @transaction(propagationType="required")   */     public void purchaseTicket(int customerAccountID, int eventID,       int quantity)   {      reserveSeats(eventID, quantity);         chargeCustomerAccount(customerAccountID, eventID, quantity);   }   ...

The preceding example shows the use of a named parameter. But because propagationType is the only parameter it seems unnecessary to have to specify its name. You can instead use an anonymous parameter as illustrated below. For this to work, though, you will have to rename the propagationType method in the Transaction interface to “value” as shown below:

   package com.devx.ticketbroker.annotation;      public interface Transaction   {       public String value();   }

After doing that, here’s the revised TicketService annotation.

   ...     /**      * @transaction("required")      */       public void purchaseTicket(int customerAccountID, int eventID,         int quantity)     {       reserveSeats(eventID, quantity);          chargeCustomerAccount(customerAccountID, eventID, quantity);     }   ...

Lastly, invoke the AnnotationC compiler as a part of the build process to compile javadoc style annotations into the produced class files:

   ...                                                                          ...

Applying Transactions to the Ticket Broker Application
Now, write an aspect to wrap calls to transactional methods with transaction management calls:

   import org.codehaus.aspectwerkz.joinpoint.JoinPoint;      import com.devx.ticketbroker.dao.HibernateUtil;      public class TransactionAspect   {      public Object aroundTransactionalAdvice(JoinPoint joinPoint)           throws Throwable       {           Object result;              System.out.println(              "AspectWerkz aspect beginning transaction");           HibernateUtil.beginTransaction();              try           {               result = joinPoint.proceed();                              System.out.println(                  "AspectWerkz aspect Committing transaction");               HibernateUtil.commitTransaction();           }           catch (Throwable e)           {               System.out.println(                  "AspectWerkz aspect rolling back transaction");               HibernateUtil.rollbackTransaction();               throw e;           }              return result;       }   }

This aspect simply uses a helper object to invoke functionality in Hibernate to begin, commit, and roll back transactions. It defines the behavior to be executed around calls to transactional methods. Next, write a pointcut to specify which methods need to be wrapped with this advice.

                                                                      

As shown above, the transactionalMethods pointcut selects all executions of methods marked with the @transaction attribute. It’s bound to the TransactionAspect class as “around” advice, meaning that the TransactionAspect will intercept execution of all methods marked for transaction support, and will surround those method calls with the appropriate transaction logic.

Lastly, you must invoke the AspectWerkzC as part of your build process to weave the transaction aspect into your application:

   ...                             ...

A re-run of the Ant test now succeeds, because all calls to purchaseTicket are now transactional:

   test:       [junit] Running           com.devx.ticketbroker.dao.impl.CustomerAccountDAOTest       [junit] Tests run: 4, Failures: 0, Errors: 0, Time elapsed: 0.687 sec       [junit] Running com.devx.ticketbroker.dao.impl.EventDAOTest       [junit] Tests run: 4, Failures: 0, Errors: 0, Time elapsed: 0.625 sec       [junit] Running com.devx.ticketbroker.service.impl.TicketServiceTest       [junit] Tests run: 3, Failures: 0, Errors: 0, Time elapsed: 0.625 sec      BUILD SUCCESSFUL

As this article shows, combining annotations with aspects is indeed possible in a pre-Java 5 world, and the combination can be a very powerful tool for managing cross-cutting concerns in your application.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist