Friday, August 03, 2007

AspectJ for lazy loading, improved

I find Russ Miles' lazy feature loading recipe (AspectJ Cookbook, chapter 21.3) cumbersome and at odds with the whole point of AOP. You have to use joinPoint.getArgs inside a proxy; but you're using AspectJ instead of Spring to avoid the proxy-based Spring AOP implementation, with all its intrinsic problems (what does "this" mean?). You also have to be aware in the mainline code that you're doing lazy loading and explicitly call LazyLoading.aspectOf().initializeFeature(), so it's intrusive.

In addition, there is essentially no reusability here. You have to create a special aspect for each interface which you wish to load lazily, with stubs for every method of that interface.

Finally, you have to use a weird trick of declaring that one interface implements another without actually giving any implementation (this line is given with no explanation in the text).

My approach, by contrast, is extremely simple. I do still impose a burden on the mainline code; specifically, if you want to see any benefit from lazy loading of an object you have to make sure the expensive initialization code actually occurs within the construction of that object. But that's it. No proxies, no hard-coded interface stubs, no intrusion.

The basic technique is to suppress the execution of the object's initialization code, wrapping it in a closure and storing it away until the first access to the object that requires that the initialization take place. The storage of the closure, and the state of the flag indicating that the initialization has or has not been attempted, live inside a small pertarget aspect.

Here's two variations of the LazyInitialization aspect.

The first:

// one aspect instance per lazily-instantiated object
public aspect LazyInitialization pertarget( target( Lazy ) ) {

// this interface represents a closure for the real initialization
private interface DelayedInit {
 void init();
}

// this is the marker interface for types that should be
// lazily initialized
public interface Lazy {};

 // per-object aspect state
 boolean flgTried = false;
 DelayedInit fnInit = null;

 // this advice intercepts the normal initialization process for
 // a Lazy object and stashes a closure for it in the aspect
 void around() : execution( Lazy+.new( .. ) ) {
  synchronized( this ) {
   if ( fnInit == null )
    fnInit = new DelayedInit() {
     public void init() {
      proceed();
     }
    };
  }
 }

  // this advice intercepts any access to a Lazy object that might
  // require it to actually initialize, and does so if necessary
  before() : call( * Lazy+.*( .. ) ) || get( * Lazy+.* ) || set( * Lazy+.* ) {
   synchronized( this ) {
    if ( !flgTried ) {
     flgTried = true;
     fnInit.init();
    }
   }
  }
}

To use it, just declare somewhere that some class or interface C implements LazyInitialization.Lazy and every instance of C or its subclasses/implementations will be lazily initialized:

 
aspect MakeCLazy {
   declare parents : C+ implements LazyInitialization.Lazy;
}

The other variation uses a generic type variable T instead of a marker interface. This version allows you to be more specific about which classes/interfaces/methods are to be lazily initialized and what constitutes an initialization-worthy access via filter pointcuts.

public abstract aspect GenericLazyInit< T > pertarget( initFilter() ) {

 // this interface represents a closure for the real initialization
 private interface DelayedInit {
  void init();
 }

 // per-object aspect state
 boolean flgTried = false;
 DelayedInit fnInit = null;

 protected pointcut initFilter() : execution( T+.new( .. ) );
 protected pointcut accessFilter() : call( * T+.*( .. ) ) || get( * T+.* ) || set( * T+.* );
 
 // this advice intercepts the normal initialization process for
 // a Lazy object and stashes a closure for it in the aspect
 void around() : initFilter() {
  synchronized( this ) {
   if ( fnInit == null )
    fnInit = new DelayedInit() {
     public void init() {
      proceed();
     }
    };
  }
 }

 // this advice intercepts any access to a Lazy object that might
 // require it to actually initialize, and does so if necessary
 before() : accessFilter() {
  synchronized( this ) {
   if ( !flgTried && fnInit != null ) {
    flgTried = true;
    fnInit.init();
   }
  }
 }
}

Using this version (in its default configuration, equivalent to variation 1) is even briefer:

 
aspect MakeCLazy extends GenericLazyInit< C > {}

1 comment:

Nicholas Hemley said...

this code works a treat since I have an example running, but is there any way to ignore the second pointcut after the object has been used for the first time, since otherwise all the calls will still be going through the pointcut, thus imposing a performance degradation? Many thanks for any ideas!