Two Fantastic Uses for AspectJ: Part One, Backward Compatibility

November 09, 2007 By Tim OBrien

4 minute read time

NOTE: I’m cross-posting this article on both my Build Chimp and Sonatype blogs.

Recently, I’ve been spending a lot of time hacking on Maven trunk (what will become Maven 2.1 eventually), and trying to tackle some of the larger problems with plugin- and extension-loading, and planning out the order of mojo execution for a given build ahead of time. While we’ve had to some really encouraging success in addressing these sorts of broad problems (along with artifact resolution, which Jason van Zyl has been paying a lot of attention to), we’ve run into other, stickier problems relating to backward compatibility.

You see, some of the more advanced Maven plugins out there make use of some pretty deeply buried Maven components to do their jobs. This means that changing the APIs of some of these deep components will break those plugins, resulting in NoSuchMethodErrors. Part of the root problem here is allowing plugins to lookup any old component in the system that they want to; another part is not publishing a single, interface-only API artifact for plugins to use that outlines exactly which facilities in the Maven core they have access to (which would then give us a target set of interfaces that must remain stable for backward compat). But regardless of the cause, this is the situation and we have to learn to live with it. As we learn lessons like this, we can improve in successive releases, but for now we still have a tricky backward-compatibility scenario to support.

So, as we change the plugin manager’s api - often deleting or adding whole methods, and even new parameter classes that don’t exist in Maven 2.0.x - I’ve come to realize that continually restoring backward compatibility in the main 2.1 codebase is always going to be a losing battle. Instead, I’m trying a new tactic: the compatibility aspect. The idea is simple, really. We can allow the main codebase to change as needed so it can adapt to improved mechanisms for transferring build state from component to component. Inevitably, these changes break backward compatibility. As we go, we should establish a test suite of plugin configurations that demonstrate a particular plugin functioning properly. When we detect a compatibility violation in one of these plugins, we can add the case to the compatibility aspect and restore backward compatibility.

As an example, the maven-help-plugin makes heavy use of the PluginManager component inside Maven’s core in order to execute the describe mojo (this mojo displays parameter and mojo information for a plugin). When we moved to passing in a MavenSession object instead of the associated local repository (an ArtifactRepository) and build Settings - since the session contains both of these pieces of information, and more - it broke the help plugin. In order to make the system look more like the Maven 2.0.x setup that the help plugin expected, I added two pointcuts and an intertype declaration. Below are the additions that allow the help plugin to continue functioning normally in a Maven 2.1 environment (please pardon the messy AspectJ syntax, I’m still learning).

First, we capture the session instance when it’s created, so we can map old methods to new ones that use it:

<code>// GRAB Session as soon as it's constructed.
private MavenSession session;

private pointcut sessionCreation( MavenSession session ):
    execution( public MavenSession+.new(..) )
    &amp;&amp; this( session )
    &amp;&amp; notHere();

// capture the session instance.
after( MavenSession session ): sessionCreation( session )
{
    this.session = session;
}
</code>

Next, we re-introduce the PluginManager method that the help plugin looks for:

<code>// Re-Introduce old verifyPlugin(..) API.
public PluginDescriptor PluginManager.verifyPlugin( Plugin plugin,
                                                    MavenProject project,
                                                    Settings settings,
                                                    ArtifactRepository localRepository )
{
    // this will always be diverted, so no need to do anything.
    return null;
}
</code>

Finally, we map the old method call over to the new one, which makes use of the captured session instance:

<code>// USE Session to compensate for old verifyPlugin(..) API.
private pointcut verifyPlugin( Plugin plugin, MavenProject project, PluginManager manager ):
    execution( public PluginDescriptor PluginManager+.verifyPlugin( Plugin,
                                                                    MavenProject,
                                                                    Settings,
                                                                    ArtifactRepository ) )
    &amp;&amp; args( plugin, project, .. )
    &amp;&amp; target( manager )
    &amp;&amp; notHere();

// redirect the old verifyPlugin(..) call to the new one,
// using the captured session instance above.
PluginDescriptor around( Plugin plugin,
                         MavenProject project,
                         PluginManager manager )
    throws ArtifactResolutionException, ArtifactNotFoundException, PluginNotFoundException,
    PluginVersionResolutionException, InvalidPluginException, PluginManagerException,
    PluginVersionNotFoundException:
        verifyPlugin( plugin, project, manager )
{
    return manager.verifyPlugin( plugin, project, session );
}
</code>

Now, we can add the AspectJ dependency and plugin to the maven build:

<code>&lt;project&gt;
  [...]

  &lt;dependencies&gt;
    [...]

    &lt;!-- Needed for backward compat aspect. --&gt;
    &lt;dependency&gt;
      &lt;groupId&gt;aspectj&lt;/groupId&gt;
      &lt;artifactId&gt;aspectjrt&lt;/artifactId&gt;
      &lt;version&gt;1.5.3&lt;/version&gt;
    &lt;/dependency&gt;
  &lt;/dependencies&gt;
  &lt;build&gt;
    &lt;plugins&gt;
      [...]

      &lt;plugin&gt;
        &lt;groupId&gt;org.codehaus.mojo&lt;/groupId&gt;
        &lt;artifactId&gt;aspectj-maven-plugin&lt;/artifactId&gt;

        &lt;executions&gt;
          &lt;execution&gt;
            &lt;id&gt;weave-compat&lt;/id&gt;
            &lt;goals&gt;
              &lt;goal&gt;compile&lt;/goal&gt;
            &lt;/goals&gt;
          &lt;/execution&gt;
        &lt;/executions&gt;
      &lt;/plugin&gt;
    &lt;/plugins&gt;
  &lt;/build&gt;
&lt;/project&gt;
</code>

When we build and install this new version of Maven now, the help plugin works without complaint.

AspectJ is an ideal tool for this sort of work, since it leaves your main codebase in pristine order. Down the road a few releases, when we no longer have to support Maven 2.0.x, we know exactly which parts of the logic catered solely to that version. We can then simply remove that aspect, and move on. By the same token, when we start making API changes for 2.2 (eventually), we can create another compatibility aspect for 2.1 functionality. This creates a non-invasive compatibility layer on top of our code, which can then evolve to meet the new requirements of improved design.

In the next post, I’ll talk about how AspectJ rescued me from a nasty memory leak.

Tags: Sonatype Says

Written by Tim OBrien

Tim is a Software Architect with experience in all aspects of software development from project inception to developing scaleable production architectures for large-scale systems during critical, high-risk events such as Black Friday. He has helped many organizations ranging from small startups to Fortune 100 companies take a more strategic approach to adopting and evaluating technology and managing the risks associated with change.