AspectJ integration for Dropwizard Metrics with optional Expression Language 3.0 (JSR-341) support.
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:
- Intercept invocations of instance and class methods annotated with
@ExceptionMetered
,@Metered
and@Timed
, - Create
Gauge
metrics for instance and class methods annotated with@Gauge
, - Register / retrieve the
Metric
instances in the resolvedMetricRegistry
instance, - Resolve the
MetricRegistry
instance by looking up into theSharedMetricRegistries
class or optionally by dynamically evaluating EL expressions.
Metrics AspectJ is compatible with Metrics version 3.0.0
+ and requires Java 6 or higher.
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.
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.
A working gradle example is available, but each integration point is described here.
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}"
}
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.
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>
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>
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.
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.
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.
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 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),
- It dynamically extends classes thus it is incompatible with
- 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.
Copyright © 2013-2016, Antonin Stefanutti
Published under Apache Software License 2.0, see LICENSE