Fork me on GitHub

AspectJ in a multi-module reactor

Strategies for using AspectJ in a Maven multi-module reactor

In a multi-module reactor, a convenient pattern for using AspectJ is to create one project containing Aspect definitions to be used by ("woven into") the code of other projects within the Maven reactor. Since some dependencies (such as the AspectJ runtime) must be identical in all the projects within the Maven reactor, this walk-through serves as a step-by-step guide to painless use of aspects in a multi-module reactor.

To make this walk-through less abstract, we will use validation as an example throughout the step-by-step guide. In this example, we will create a small API for validating the internal state of objects, and delegate its correct use to AspectJ by means of the AspectJ Maven Plugin. There are many ways besides AspectJ to implement this, but AspectJ is one of the few which does not depend on particular surroundings such as containers or application servers. In fact, the small validation solution may be run directly in a plain Java SE environment. All steps will be detailed in the walk-through below, but we will start by taking a look at the end result:

multimodule ajc

The reactor contains the following parts:

  • rootParentPOM: Contains version definitions for the aspectjrt and aspectjtools libraries, to be used in all modules in the multi-module reactor.

  • validation-api: Defines a small API for validating the internal state of objects.

  • validation-aspect: Defines an aspect using classes from the validation-api to validate the internal state of objects.

  • aspectParentPOM: Configures the AspectJ Maven Plugin (this plugin!) to use the aspect defined within the validation-aspect module and apply it on all Maven modules using aspectParentPOM as a parent POM.

  • mavenProjectWithAspects: A project whose classes can use/implement types from the validation-api to indicate that the aspect defined in validation-aspect should be woven into its bytecode representation.

After the introduction, let us proceed to the step-by-step guide.

Define required runtime dependency versions

In the rootParentPom, define dependencies for the AspectJ runtime and your aspect library. It is recommended to introduce a Maven property in order to reduce the number of locations where the AspectJ version must be changed to upgrade the dependency.

<!-- +========================================= -->
<!-- | Maven property definitions               -->
<!-- +========================================= -->
<properties>
  <aspectj.version>1.9.21</aspectj.version>
  <!-- ... -->
</properties>

<!-- +========================================= -->
<!-- | Dependency (management) settings         -->
<!-- +========================================= -->
<dependencyManagement>
  <dependencies>
    <!-- AOP dependencies. -->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjrt</artifactId>
      <version>${aspectj.version}</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjtools</artifactId>
      <version>${aspectj.version}</version>
    </dependency>
    <!-- ... -->
  </dependencies>
</dependencyManagement>

The definitions in the rootParentPOM are now complete.

Implement the validation API

For the purpose of this example, we would like AspectJ to enforce a common behaviour/lifecycle for objects whose internal state can be validated. The validation API contains a defining interface Validatable, to be implemented by any class whose instances should be validated after creation.

/**
 * Specification for providing an object with validation mechanics.
 * Each Validatable object can perform internal validation to assert
 * its state before being serialized and after being deserialized.
 *
 * Making an object implement Validatable does not imply that all
 * uses of the object is guaranteed. Validatable objects should
 * primarily make use of their own data to ascertain its valid state.
 *
 * It is the responsibilities of services using the Validatable object
 * (as opposed to the validation mechanics provided within this Validatable)
 * to provide extra/semantic validation for object <strong>graphs</strong>
 * in which this Validatable instance is part.
 */
public interface Validatable {
  /**
   * Performs validation of the internal state of this Validatable.
   *
   * @throws InternalStateValidationException
   *          if the state of this Validatable was
   *          in an incorrect state (i.e. invalid).
   */
  void validateInternalState() throws InternalStateValidationException;
}

Following the definition of a custom InternalStateValidationException extending IllegalStateException, the small validation-api module is complete:

/**
 * Exception indicating problems occurred when validating a Validatable instance.
 */
public class InternalStateValidationException extends IllegalStateException {
  /**
   * Constructs an InternalStateValidationException with the specified detail message.
   * A detail message is a String that describes this particular exception.
   *
   * @param message the String that contains a detailed message
   */
  public InternalStateValidationException(final String message) {
    super(message);
  }
}

Assuming that the rootParentPOM contains common build definitions which should be applied to the validation-api module, we should remember to assign the rootParentPOM as its parent.

In this brief example, we explore no reason to connect the validation-api to a parent - but in real life configurations such as licensing, coverage, code style, etc. are frequently configured and maintained only in one POM - the rootParentPOM.

Implement the validation aspect

Now that our tiny validation API is complete, it is time to implement its corresponding aspect. This happens in the validation-aspect module, which must import the validation-api as a dependency in its POM. It is also important, that the validation-aspect module assigns its POM parent to rootParentPOM, since we want to use the AspectJ dependencies managed in the parent. The POM bits of the validation-aspect project are shown below:

<!-- +========================================= -->
<!-- | Define the Parent POM                    -->
<!-- +========================================= -->
<parent>
  <groupId>some.group.id</groupId>
  <artifactId>rootParentPOM</artifactId>
  <version>1.0.0-SNAPSHOT</version>
</parent>

<!-- +========================================= -->
<!-- | Dependency (management) settings         -->
<!-- +========================================= -->
<dependencies>
  <dependency>
    <groupId>some.group.id</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </dependency>
  <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
  </dependency>
</dependencies>

<!-- +========================================= -->
<!-- | Build settings                           -->
<!-- +========================================= -->
<build>
  <plugins>
    <plugin>
      <groupId>${project.groupId}</groupId>
      <artifactId>aspectj-maven-plugin</artifactId>
      <version>${project.version}</version>
      <configuration>
        <complianceLevel>8</complianceLevel>
      </configuration>
      <executions>
        <execution>
          <id>compile-with-aspectj</id>
          <goals>
            <goal>compile</goal>
            <goal>test-compile</goal>
          </goals>
        </execution>
      </executions>
      <dependencies>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjtools</artifactId>
          <version>${aspectj.version}</version>
        </dependency>
      </dependencies>
    </plugin>
  </plugins>
</build>

Besides including the correct parent POM and the required dependencies on aspectjrt and validation-api, the validation-aspect POM must also configure aspectj-maven-plugin to perform AspectJ compilation.

Note that aspectjtools should be included as a plugin dependency for aspectj-maven-plugin to ensure that the same version of AspectJ is used during compilation and runtime.

The aspect implementation itself may appear somewhat complex, despite only having a single active method. The crucial method call is validatable.validateInternalState();, which appears towards the end of method performValidationAfterCompoundConstructor. This is the place, where this Aspect invokes the method from the validation API, and therefore lets the object validate its internal state.

The other two methods in the aspect are placeholders for two pointcut expressions defining exactly when the advice should be triggered. In this example, we want to invoke validation after any constructor other than a default constructor is called.

Why would you not want to perform validation after a default constructor is called? Consider the lifecycle for frameworks which create an object instance by calling the default constructor of a class, followed by populating its internal state (i.e. its private members) using reflection. Some such frameworks include JAXB and JPA, implying that validation cannot be performed immediately after a default constructor has been run since the state of the object is still empty.

The aspect is implemented as a standard Java class using AspectJ annotations, as shown below:

/**
 * The aspect enforcing validity on a class implementing Validatable (i.e. Entities).
 * This aspect should be fired immediately after a non-default constructor is invoked,
 * and is intended to run as a singleton.
 * <p>
 * Validation should be run only once, and only after the constructor of the ultimate
 * created instance is run (default AspectJ behaviour is to run the Aspect after any
 * constructor within the inheritance hierarchy is executed [i.e. after constructors
 * in superclasses are run, within the constructor of subtypes]).
 */
@Aspect
public class ValidationAspect {
  // Our log
  private static final Logger log = LoggerFactory.getLogger(ValidationAspect.class);

  /**
   * Pointcut defining a default constructor within any class.
   */
  @Pointcut("initialization(*.new())")
  void anyDefaultConstructor() {}

  /**
   * Defines a Pointcut for any constructor in a class implementing Validatable -
   * except default constructors (i.e. those having no arguments).
   *
   * @param joinPoint    The currently executing joinPoint.
   * @param aValidatable The Validatable instance just created.
   */
  @Pointcut(
    value = "initialization(se.jguru.nazgul.tools.validation.api.Validatable+.new(..)) "
      + "&& this(aValidatable) "
      + "&& !anyDefaultConstructor()", argNames = "joinPoint, aValidatable"
  )
  void anyNonDefaultConstructor(final JoinPoint joinPoint, final Validatable aValidatable) {}

  /**
   * Validation aspect, performing its job after calling any constructor except
   * non-private default ones (having no arguments).
   *
   * @param joinPoint   The currently executing joinPoint.
   * @param validatable The validatable instance just created.
   * @throws InternalStateValidationException if the validation of the validatable failed.
   */
  @AfterReturning(value = "anyNonDefaultConstructor(joinPoint, validatable)", argNames = "joinPoint, validatable")
  public void performValidationAfterCompoundConstructor(final JoinPoint joinPoint, final Validatable validatable)
    throws InternalStateValidationException
  {

    if (log.isDebugEnabled()) {
      log.debug("Validating instance of type [" + validatable.getClass().getName() + "]");
    }

    if (joinPoint.getStaticPart() == null) {
      log.warn("Static part of join point was null for validatable of type: "
                 + validatable.getClass().getName(), new IllegalStateException());
      return;
    }

    // Ignore calling validateInternalState when we execute constructors in
    // any class but the concrete Validatable class.
    final ConstructorSignature sig = (ConstructorSignature) joinPoint.getSignature();
    final Class<?> constructorDefinitionClass = sig.getConstructor().getDeclaringClass();

    if (validatable.getClass() == constructorDefinitionClass) {
      // Now fire the validateInternalState method.
      validatable.validateInternalState();
    }
    else {
      log.debug("Ignored firing validatable for constructor defined in ["
                  + constructorDefinitionClass.getName() + "] and Validatable of type ["
                  + validatable.getClass().getName() + "]");
    }
  }
}

Include aspects into aspectParentPOM

The last piece of the AspectJ plugin puzzle is the inclusion of our validation aspect into the standard build cycle, which is performed within the aspectParentPOM module. The aspectParentPOM must therefore include a dependency on the validation-aspect project, and use the rootParentPOM as a parent.

<!-- +========================================= -->
<!-- | Define the Parent POM                    -->
<!-- +========================================= -->
<parent>
  <groupId>some.group.id</groupId>
  <artifactId>rootParentPOM</artifactId>
  <version>1.0.0-SNAPSHOT</version>
</parent>

<!-- +========================================= -->
<!-- | Dependency (management) settings         -->
<!-- +========================================= -->
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>some.group.id</groupId>
      <artifactId>validation-aspect</artifactId>
      <version>1.0.0-SNAPSHOT</version>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <!-- Include AOP dependencies -->
  <dependency>
    <groupId>some.group.id</groupId>
    <artifactId>validation-aspect</artifactId>
  </dependency>
  <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
  </dependency>
</dependencies>

<!-- +========================================= -->
<!-- | Build settings                           -->
<!-- +========================================= -->
<build>
  <plugins>
    <plugin>
      <groupId>${project.groupId}</groupId>
      <artifactId>aspectj-maven-plugin</artifactId>
      <version>${project.version}</version>
      <configuration>
        <complianceLevel>8</complianceLevel>
        <aspectDirectory>src/main/aspect</aspectDirectory>
        <testAspectDirectory>src/test/aspect</testAspectDirectory>
        <XaddSerialVersionUID>true</XaddSerialVersionUID>
        <showWeaveInfo>true</showWeaveInfo>
        <aspectLibraries>
          <aspectLibrary>
            <groupId>some.group.id</groupId>
            <artifactId>validation-aspect</artifactId>
          </aspectLibrary>
        </aspectLibraries>
      </configuration>
      <executions>
        <execution>
          <id>compile-with-aspectj</id>
          <goals>
            <goal>compile</goal>
            <goal>test-compile</goal>
          </goals>
        </execution>
      </executions>
      <dependencies>
        <dependency>
          <groupId>org.aspectj</groupId>
          <artifactId>aspectjtools</artifactId>
          <version>${aspectj.version}</version>
        </dependency>
      </dependencies>
    </plugin>
  </plugins>
</build>

For all modules using the aspectParentPOM as their parent, the validation aspect will be woven into all classes implementing Validatable. This means, you can perform automatic AspectJ-driven validation in objects inside or outside any container.