AspectJ integration for Dropwizard Metrics
Clone or download

README.md

AspectJ for Metrics

Build Status Coverage Status Maven Central

AspectJ integration for Dropwizard Metrics with optional Expression Language 3.0 (JSR-341) support.

About

Metrics AspectJ provides support for the Metrics annotations in Java SE environments using AspectJ to perform AOP instrumentation. It implements the contract specified by these annotations with the following level of functionality:

Metrics AspectJ is compatible with Metrics version 3.0.0+ and requires Java 6 or higher.

Getting Started

Using Maven

Add the metrics-aspectj library as a dependency:

<dependency>
    <groupId>io.astefanutti.metrics.aspectj</groupId>
    <artifactId>metrics-aspectj</artifactId>
    <version>1.2.0</version>
</dependency>

And configure the maven-aspectj-plugin to compile-time weave (CTW) the metrics-aspectj aspects into your project:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <configuration>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>io.astefanutti.metrics.aspectj</groupId>
                <artifactId>metrics-aspectj</artifactId>
            </aspectLibrary>
        </aspectLibraries>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

More information can be found in the Maven AspectJ plugin documentation.

Using Ant

Use the AjcTask (iajc) Ant task:

<target name="{target}" >
    <iajc sourceroots="${basedir}/src"
          classpath="${basedir}/lib/aspectjrt.jar"
          outjar="${basedir}/build/${ant.project.name}.jar">
        ...
        <aspectpath>
            <pathelement location="${basedir}/lib/metrics-aspectj.jar"/>
        </aspectpath>
        ...
    </iajc>
</target>

Other options are detailed in the AspectJ Ant tasks documentation.

Using Gradle

A working gradle example is available, but each integration point is described here.

build.gradle snippets
buildscript {
    // ensure the gradle-aspectj integration is w/i the build classpath
    dependencies {
        classpath 'nl.eveoh:gradle-aspectj:1.6'
    }
}

// specify the aspectjVersion, used by gradle-aspectj
project.ext {
    aspectjVersion = '1.8.10'
}

// specify the Dropwizard Metrics version (metricsVer)
//  and the aspect-oriented metrics version (metricsAspectVer, this solution)
ext {
    metricsVer = '3.2.2'
    metricsAspectVer = '1.2.0'
}

// via the gradle-aspectj integration, run "aspect weaving"
apply plugin: 'aspectj'

// ensure Dropwizard Metrics as well as the aspect-oriented metrics (astefanutti.metrics.aspectj)
//  runtime dependencies of your solution are satisfied.
dependencies {
    compile "io.astefanutti.metrics.aspectj:metrics-aspectj:${metricsAspectVer}"
    // add a path for the gradle-aspectj "aspect weaving" (AspectJ Compiler compile)
    aspectpath "io.astefanutti.metrics.aspectj:metrics-aspectj:${metricsAspectVer}"

    compile "io.dropwizard.metrics:metrics-core:${metricsVer}"
    compile "io.dropwizard.metrics:metrics-annotation:${metricsVer}"
}

Using the AspectJ Compiler

The AspectJ compiler can be used directly by executing the following command:

ajc -aspectpath metrics-aspectj.jar [Options] [file...]

More information can be found in the AspectJ compiler / weaver documentation.

Required Dependencies

Besides depending on Metrics (metrics-core and metrics-annotation modules), Metrics AspectJ requires the AspectJ aspectjrt module:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
</dependency>

These three modules are transitive dependencies of the metrics-aspectj Maven module.

Alternatively, the metrics-aspectj-deps artifact that re-packages the metrics-annotation and the aspectjrt modules can be used so that the only required dependency is metrics-core:

<dependency>
    <groupId>io.astefanutti.metrics.aspectj</groupId>
    <artifactId>metrics-aspectj-deps</artifactId>
</dependency>

Optional Dependencies

In addition to that, Metrics AspectJ optional support of EL 3.0 expression for MetricRegistry resolution and Metric name evaluation requires an implementation of Expression Language 3.0 (JSR-341) to be present at runtime. For example, the metrics-aspectj-el module is using the GlassFish reference implementation as test dependency for its unit tests execution:

<dependency>
    <groupId>org.glassfish</groupId>
    <artifactId>javax.el</artifactId>
</dependency>

Usage

Metrics AspectJ Activation

In order to activate Metrics AspectJ for a particular class, it must be annotated with the @Metrics annotation:

import com.codahale.metrics.annotation.Timed;

import io.astefanutti.metrics.aspectj.Metrics;

@Metrics
class TimedMethod {

    @Timed(name = "timerName")
    void timedMethod() {} // Timer name => TimedMethod.timerName
}

At weaving time, Metrics AspectJ will detect the @Metrics annotation, scan all the declared methods of the target class that are annotated with Metrics annotations, then create and register the corresponding Metric instances and weave its aspects around these methods. At runtime, these Metric instances will eventually get called according to the Metrics annotations specification.

Note that Metrics annotations won't be inherited if declared on an interface or a parent class method. More details are available in the Limitations section.

The Metrics Annotations

Metrics comes with the metrics-annotation module that contains a set of annotations and provides a standard way to integrate Metrics with frameworks supporting Aspect Oriented Programming (AOP). These annotations are supported by Metrics AspectJ that implements their contract as documented in their Javadoc.

For example, a method can be annotated with the @Timed annotation so that its execution can be monitored using Metrics:

import com.codahale.metrics.annotation.Timed;

import io.astefanutti.metrics.aspectj.Metrics;

@Metrics
class TimedMethod {

    @Timed(name = "timerName")
    void timedMethod() {} // Timer name => TimedMethod.timerName
}

In that example, Metrics AspectJ will instrument all the constructors of the TimedMethod class by injecting Java bytecode that will automatically create a Timer instance with the provided name (or retrieve an existing Timer with the same name already registered in the MetricRegistry) right after the instantiation of the TimedMethod class and inline the method invocation around with the needed code to time the method execution using that Timer instance.

A static method can also be annotated with the @Timed annotation so that its execution can be monitored using Metrics:

import com.codahale.metrics.annotation.Timed;

import io.astefanutti.metrics.aspectj.Metrics;

@Metrics
class TimedMethod {

    @Timed(name = "timerName")
    static void timedStaticMethod() {} // Timer name => TimedMethod.timerName
}

In that example, Metrics AspectJ will instrument the TimedMethod class so that, when it's loaded, a Timer instance with the provided name will be created (or an existing Timer with the same name already registered in the MetricRegistry will be retrieved) and inline the method invocation around with the needed code to time the method execution using that Timer instance.

Optionally, the Metric name can be resolved with an EL expression that evaluates to a String:

import com.codahale.metrics.annotation.Timed;

import io.astefanutti.metrics.aspectj.Metrics;

@Metrics
class TimedMethod {

    private long id;

    public long getId() {
        return id;
    }

    @Timed(name = "timerName ${this.id}")
    void timedMethod() {} // Timer name => TimedMethod.timerName <id>
}

In that example, Metrics AspectJ will automatically create a Timer instance (respectively retrieve an existing Timer instance with the same name already registered in the MetricRegistry) right after the instantiation of the TimedMethod class and evaluate the EL expression based on the value of the id attribute of that newly created TimedMethod instance to name the Timer instance (respectively resolve the Timer instance registered in the MetricRegistry). If the value of the id attribute changes over time, the name of the Timer instance won't be re-evaluated.

Note that these annotations won't be inherited if they are placed on interface or parent class methods. Indeed, according to the Java language specification, non-type annotations are not inherited. It is discussed in more details in the Limitations section.

Metrics Registry Resolution

The Metrics.registry annotation attribute provides the way to declare the MetricRegistry to register the generated Metric instances into. Its value can either be a string literal that identifies a MetricRegistry accessible by name from the SharedMetricRegistries class or a valid EL expression that evaluates to the registry name or the registry instance. The resultant MetricRegistry is used to register the Metric instantiated into each time a Metrics annotation is present on that class methods. It defaults to the string literal metrics-registry.

The MetricRegistry can thus be resolved by name relying on the SharedMetricRegistries.getOrCreate(String name) method:

import com.codahale.metrics.annotation.Metered;

import io.astefanutti.metrics.aspectj.Metrics;

@Metrics(registry = "registryName")
class MeteredMethodWithRegistryByName {

    @Metered(name = "meterName")
    void meteredMethod() {} // Registry => SharedMetricRegistries.getOrCreate("registryName")
}

Or with an EL expression that evaluates to a bean property of type MetricRegistry:

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.annotation.Metered;

import io.astefanutti.metrics.aspectj.Metrics;

@Metrics(registry = "${this.registry}")
class MeteredMethodWithRegistryFromProperty {

    final MetricRegistry registry;

    MeteredMethodWithRegistryFromProperty(MetricRegistry registry) {
        this.registry = registry;
    }

    MetricRegistry getRegistry() {
        return registry;
    }

    @Metered(name = "meterName")
    void meteredMethod() {} // Registry => this.getRegistry()
}

Or with an EL expression that evaluates to a String. In that case the registry is resolved by name using the SharedMetricRegistries.getOrCreate(String name) method.

Limitations

The Metrics annotations are not inherited whether these are declared on a parent class or an implemented interface method. The root causes of that limitation, according to the Java language specification, are:

  • Non-type annotations are not inherited,
  • Annotations on types are only inherited if they have the @Inherited meta-annotation,
  • Annotations on interfaces are not inherited irrespective to having the @Inherited meta-annotation.

See the @Inherited Javadoc and Annotation types from the Java language specification for more details.

AspectJ follows the Java language specification and has documented to what extent it's impacted in Annotation inheritance and Annotation inheritance and pointcut matching. There would have been ways of working around that though:

  • That would have been working around the Java language specification in the first place,
  • Plus that would have required to rely on a combination of Expression-based pointcuts, Runtime type matching and Reflective access to define conditional pointcut expressions which:
    • Would have widen the scope of matching joint points thus introducing side-effects in addition to being inefficient,
    • Would have been evaluated at runtime for each candidate join point relying on the Java Reflection API thus impacting the application performance and incidentally voiding the non-intrusive benefit of AOP in a larger sense.

Spring AOP vs. AspectJ

Spring AOP and AspectJ provides Aspect Oriented Programming (AOP) in two very different ways:

  • AspectJ provides a full-fledged aspect definition and support both Compile Time Weaving (CTW) and Load Time Weaving (LTW) (with a Java agent) and implements AOP with class instrumentation (byte code manipulation),
  • Spring AOP does not support the whole AspectJ aspect definition and does not support Compile Time Weaving,
  • Spring AOP implements AOP either using (see Spring proxying mechanisms):
    • JDK dynamic proxies, which add little runtime overhead, clutter stack traces and can be incompatible with other Spring functionality like Spring JMX (for dynamic MBean export for example),
    • Or CGLIB (byte code manipulation), that has to be added as a runtime dependency:
      • It dynamically extends classes thus it is incompatible with final classes or methods,
      • CGLIB development isn't active, Hibernate has been deprecating it in favor of Javassist (see Deprecated CGLIB support),
  • AJDT (AspectJ Development Tools) provides deep integration between AspectJ and the Eclipse platform which is not possible with Spring AOP due to the runtime / dynamic nature of its AOP implementation.

Further details can be found in Choosing which AOP declaration style to use from the Spring framework documentation. The Spring AOP vs AspectJ question on Stack Overflow provides some insights as well.

License

Copyright © 2013-2016, Antonin Stefanutti

Published under Apache Software License 2.0, see LICENSE