NOTE: I’m cross-posting this article on both my Build Chimp and Sonatype blogs.
I mentioned recently in part one that using AspectJ to support backward compatibility for Maven is proving a great way to keep the main API free of the clutter associated with supporting old method signatures and classpath structures (for instance, making sure plexus-utils is always available, as it once was from Maven’s core). Today, I’d like to shed a little light on some other aspecting work I’ve done that bridges two previously unconnected manager components inside the Plexus container, to support active component collections.
Active component collections are maps, lists, etc. of Plexus components which can themselves be injected as requirements into yet another component when it traverses the CompositionPhase, the phase of the Plexus component lifecycle where dependency injection typically takes place. Without getting too far into the details, these collections are not real components in the Plexus sense of the word. They cannot be released using the PlexusContainer.release(..)
method, for instance. Since there is no release mechanism for cleaning up these collections, the components referenced within them are also not released when the owning component is released. If the components within the collection use Plexus’ keep-alive variant of the singleton instantiation pattern, it means that component will live on until the disposal of its parent PlexusContainer instance…in some cases, a very long time. As I alluded to in part one, this can lead to a pretty nasty memory leak, particularly if the container is used with a multitude of very short-lived child classloaders from which components are loaded (as in the case with Maven, where each plugin must have a semi-isolated environment in which to execute). Before this fix, the introduction of component collections limited Maven builds using the default 64M of heap memory to building no more than two or three module projects at a time…a very nasty memory leak.
If I were designing a virgin API, I would introduce an eventing system that would fire an event to all registered listeners each time a component was released. However, the overhead in terms of new code to implement this sort of system would introduce quite a bit of risk to a system we’re trying to keep relatively stable (at least for now). Instead, I was able to couple the owning component’s lifecycle to that of the component collection using AspectJ, achieving in about 120 lines of code what would have taken several classes to introduce conventionally.
To implement this sort of bridging behavior in AspectJ, I first had to declare a new field on the PlexusContainer itself, so I can track component collection instances:
<code> private Map PlexusContainer.collectionsByComponent = new HashMap(); </code>
NOTE: I tied the collectionsByComponent Map to the container instance, to give it roughly the same lifecycle as its parent container, rather than living outside in some vague singleton state as part of the aspect, and potentially causing cross-pollination between containers.
Now that we have the component collections map, we have to populate it:
<code> private pointcut inContainer( PlexusContainer container ): execution( public * PlexusContainer+.*( .. ) ) && this( container ); private pointcut componentMapCreation( Object hostComponent, ComponentMap collection ): execution( ComponentMap.new( .. ) ) && cflow( execution( * AbstractComponentComposer.findRequirement( Object, .. ) ) ) && args( hostComponent, .. ) && this( collection ); private pointcut componentMapCreationWormhole( Object hostComponent, ComponentMap collection, PlexusContainer container ): cflowbelow( inContainer( container ) ) && componentMapCreation( hostComponent, collection ); after( Object hostComponent, ComponentMap collection, PlexusContainer container ): componentMapCreationWormhole( hostComponent, collection, container ) { Set collections = (Set) container.collectionsByComponent.get( hostComponent ); if ( collections == null ) { collections = new HashSet(); container.collectionsByComponent.put( hostComponent, collections ); } collections.add( collection ); } private pointcut componentListCreation( Object hostComponent, ComponentList collection ): execution( ComponentList.new( .. ) ) && cflow( execution( * AbstractComponentComposer.findRequirement( Object, .. ) ) ) && args( hostComponent, .. ) && this( collection ); private pointcut componentListCreationWormhole( Object hostComponent, ComponentList collection, PlexusContainer container ): cflowbelow( inContainer( container ) ) && componentListCreation( hostComponent, collection ); after( Object hostComponent, ComponentList collection, PlexusContainer container ): componentListCreationWormhole( hostComponent, collection, container ) { Set collections = (Set) container.collectionsByComponent.get( hostComponent ); if ( collections == null ) { collections = new HashSet(); container.collectionsByComponent.put( hostComponent, collections ); } collections.add( collection ); } </code>
Okay, that’s not so hard. Now, we have a handle to each component collection in play, keyed by its host component instance. Note that we’re relying on that component to have functional identity methods - equals(..)
and hashcode()
. However, that’s a relatively common assumption in Plexus, so I chose it again for consistency here.
Next, we have to detect the release of the host component, and clear out all related component collections. The clear()
method of both types of component collection will release all component references it is hanging onto, along with caches of component descriptors, etc. We can trigger this cleanup with the following:
<code> private pointcut activeCollectionOwnerReleased( Object component ): call( void ComponentManager+.release( Object ) ) && args( component ); private pointcut activeCollectionOwnerReleasedWormhole( Object component, PlexusContainer container ): cflowbelow( inContainer( container ) ) && activeCollectionOwnerReleased( component ); after( Object component, PlexusContainer container ): activeCollectionOwnerReleasedWormhole( component, container ) { Set collections = (Set) container.collectionsByComponent.remove( component ); if ( collections != null ) { for ( Iterator it = collections.iterator(); it.hasNext(); ) { AbstractComponentCollection collection = (AbstractComponentCollection) it.next(); collection.clear(); } } } </code>
Finally, when the container instance itself is disposed, we should probably perform one more exhaustive cleanup of all the component collections it manages:
<code> private pointcut containerStopped( PlexusContainer container ): execution( void PlexusContainer+.dispose() ) && this( container ); after( PlexusContainer container ): containerStopped( container ) { for ( Iterator collectionSetIterator = container.collectionsByComponent.values().iterator(); collectionSetIterator.hasNext(); ) { Set collections = (Set) collectionSetIterator.next(); if ( collections != null ) { for ( Iterator it = collections.iterator(); it.hasNext(); ) { AbstractComponentCollection collection = (AbstractComponentCollection) it.next(); collection.clear(); } } } container.collectionsByComponent.clear(); container.collectionsByComponent = null; } </code>
Now that we’re finished with the code, we simply add the aspectj-maven-plugin to the Plexus pom.xml
:
<code> <project> [...] <dependencies> [...] <!-- Needed for backward compat aspect. --> <dependency> <groupId>aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.5.3</version> </dependency> </dependencies> <build> <plugins> [...] <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <executions> <execution> <id>weave-compat</id> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project> </code>
Then, rebuild the container project, and we’re in business!
AspectJ is proving a very powerful tool for dealing with these sorts of emergency, low-risk fixes, in particular because of the wormhole pattern displayed above. Using this pattern, I was able to collection various pieces of context (used as parameters to the advice) and combine them to produce the desired behavior. Using conventional OO techniques, I would have been forced to supply either new parameters to pass the context through many, many levels of code into the collection; implement a whole eventing subsystem to bridge relevant pieces of the system together, or provide some sort of universally-accessible query interface to allow instances to retrieve the information needed for this sort of cleanup. Of these, only the eventing system looks even remotely interesting from an architecture point of view; the others quickly grow out of hand, as you lose the ability to know which objects refer to which other objects (in the case of the query interface), or as your parameter lists multiply like rabbits on a warm spring day. As far as the eventing system, I’m sure you’ll agree that it’s a pretty heavy solution for the single use case that currently demands it.
Technorati Tags:
maven
plexus
aspectj
wormhole