Tuesday, November 26, 2013

Weaving with AspectJ

I talked before about the most popular problems new spring users experience with spring aop (problem #1problem #2). I also said that aspectj weaving completely eliminates them. So, I'm going to briefly explain what is aspectj and show how to use it.

AspectJ claims that it is a 'seamless aspect-oriented extension to the Java programming language that enables clean modularization of these crosscutting concerns'. I.e. it is a framework that allows to define aspects at particular manner and inject corresponding instructions directly to the byte code. Historically there was a dedicated aspect description language and extended java compiler that was able to understand it. AspectJ guys also introduced ability to define aspects via java5 annotations later. Feel free to get more information about AspectJ facilities and syntax at the AspectJ documentation page.

The main difference between Spring AOP and AspectJ AOP is that Spring AOP is proxy-based, i.e. it assumes that the client uses AOP-aware proxies instead of the 'raw' objects. That causes the problem I mentioned before. AspectJ injects its instructions directly to the byte code, hence, it doesn't suffer from that.

Spring users can witch to AspectJ immediately in the case of Spring2 AOP usage - spring uses subset of AspectJ pointcut expression language, and @AspectJ spring aspects are fully eligible for AspectJ weaving.

Lets define a simple test-case that shows AspectJ weaving:

TestTarget.java
package com.aspectj;

public class TestTarget {

    public static void main(String[] args) {
        System.out.println("----------------------->--------- Start test -----------<--------------------- span="">);
        new TestTarget().test();
        System.out.println("----------------------->--------- End test -----------<--------------------- span="">);
    }

    public void test() {
        System.out.println("TestTarget.test()");
    }
}


TestAspect.java
package com.aspectj;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;

@Aspectpublic class TestAspect {

    @Before("execution (* com.aspectj.TestTarget.test*(..))")
    public void advice(JoinPoint joinPoint) {
        System.out.printf("TestAspect.advice() called on '%s'%n", joinPoint);
    }
}


What do we want is to see that aspect method is called when TestTarget.test() is invoked.

There are three ways to inject instructions implied by AspectJ aspects:

  • compile-time weaving - compile either target source or aspect classes via dedicated aspectj compiler;

  • post-compile weaving - inject aspect instructions to already compiled classes;

  • load-time weaving - inject aspect instructions to the byte code during class loading, i.e. load instrumented class instead of the 'raw' one;


It's possible to use any of the approaches mentioned above via various ways. I'm big fan of the law of leaky abstractions, so, lets perform weaving at the lowest level at first.

Lets define our directories structure for the example:



Here *.jar files are AspectJ binaries:

  • aspectjrt.jar - necessary in runtime for correct aspects processing;

  • aspectjtools.jar - contains implementation of aspectj compiler;

  • aspectjweaver.jar - bridge between aspectj logic and java instrumentation;


*.xml files are:

  • aop.xml - aspectj loadtime descriptor;

  • build.xml - ant script;

  • pom.xml - maven descriptor;


Command-line weaving


Compile-time and post-compile-time weaving is performed via ajc tool that stands for aspectj compiler. It allows to weave aspects at compile-time. Feel free to read more about it at its documentation.

Compile-time weaving



compile-time-weaving.sh
#!/bin/bash

# Prepare
echo "Preparing the environment..."
rm -rf ./target 2>/dev/null
CLASSES_DIR=./target/classes/compile-time
COUNTER=1
CURRENT_DIR=
while :
do
    DIR=`echo "$CLASSES_DIR" | cut -d'/' -f $COUNTER`
    test "$DIR" = "" && break
    CURRENT_DIR=${CURRENT_DIR}${DIR}/
    mkdir $CURRENT_DIR 2>/dev/null
    COUNTER=`expr $COUNTER + 1`
done

CLASSPATH=./src/main/java
for i in 'aspectjtools.jar' 'aspectjrt.jar'
do
    CLASSPATH=$CLASSPATH:./src/main/resources/$i
done

# Compile the sources
echo "Compiling..."
java -cp $CLASSPATH org.aspectj.tools.ajc.Main -source 1.5 -d $CLASSES_DIR src/main/java/com/aspectj/TestTarget.java src/main/java/com/aspectj/TestAspect.java

# Run the example and check that aspect logic is applied
echo "Running the sample..."
java -cp $CLASSPATH:$CLASSES_DIR com.aspectj.TestTarget


It compiles target class and aspect class and runs target class. Following output is produced:
denis@harmony:/storage/projects/java/test$ ./compile-time-weaving.sh 
Preparing the environment...
Compiling...
Running the sample...
----------------------->--------- Start test -----------<--------------------- br="">TestAspect.advice() called on 'execution(void com.aspectj.TestTarget.test())'
TestTarget.test()
----------------------->--------- End test -----------<--------------------- span="">


We can see that aspect logic is introduced to the target class.

Note: it is assumed that java remains at the path.

Post-compile weaving



The general idea here is to inject aspect logic to the existing binaries. It's very useful when you work with third-party libraries. AspectJ keeps original byte code untouched and produces the new one with aspect logic inside it.

post-compile-weaving.sh script shows that approach:
#!/bin/bash

function ensure-dir-exists {
    COUNTER=1
    CURRENT_DIR=
    while :
    do
        DIR=`echo "$1" | cut -d'/' -f $COUNTER`
        test "$DIR" = "" && break
        CURRENT_DIR=${CURRENT_DIR}${DIR}/
        mkdir $CURRENT_DIR 2>/dev/null
        COUNTER=`expr $COUNTER + 1`
    done
}

# Prepare
echo "Preparing the environment..."
rm -rf ./target 2>/dev/null
JAR_DIR=./target/classes/post-compile-time
ensure-dir-exists $JAR_DIR

CLASSES_DIR=./target/classes/pure
ensure-dir-exists $CLASSES_DIR

CLASSPATH=./src/main/java
for i in 'aspectjtools.jar' 'aspectjrt.jar'
do
    CLASSPATH=$CLASSPATH:./src/main/resources/$i
done

# Compile the sources
echo "Compiling..."
javac -classpath $CLASSPATH -g -d $CLASSES_DIR src/main/java/com/aspectj/TestTarget.java src/main/java/com/aspectj/TestAspect.java

echo "Weaving aspect..."
java -cp $CLASSPATH org.aspectj.tools.ajc.Main -source 1.5 -inpath $CLASSES_DIR -aspectpath ./src/main/java -outjar $JAR_DIR/test.jar

# Run the example and check that aspect logic is applied
echo "Running the sample..."
java -cp $CLASSPATH:$JAR_DIR/test.jar com.aspectj.TestTarget


This script compiles sources using standard javac compiler and weaves the aspects to the binary code. The output shows that aspect is correctly woven.

Load-time weaving



Aspects logic is injected to the class byte code during loading classes to the JVM. Standard java instrumentation facilitiesare used for that. More information about load-time weaving may be found here.

load-time-weaving.sh contains the following instructions:
#!/bin/bash

function ensure-dir-exists {
    COUNTER=1
    CURRENT_DIR=
    while :
    do
        DIR=`echo "$1" | cut -d'/' -f $COUNTER`
        test "$DIR" = "" && break
        CURRENT_DIR=${CURRENT_DIR}${DIR}/
        mkdir $CURRENT_DIR 2>/dev/null
        COUNTER=`expr $COUNTER + 1`
    done
}

# Prepare
echo "Preparing the environment..."
rm -rf ./target 2>/dev/null
CLASSES_DIR=./target/classes/pure
ensure-dir-exists $CLASSES_DIR

CLASSPATH=./src/main/java
for i in 'aspectjweaver.jar' 'aspectjrt.jar'
do
    CLASSPATH=$CLASSPATH:./src/main/resources/$i
done

# Compile the sources
echo "Compiling..."
javac -classpath $CLASSPATH -g -d $CLASSES_DIR src/main/java/com/aspectj/TestTarget.java src/main/java/com/aspectj/TestAspect.java

# Run the example and check that aspect logic is applied
echo "Running the sample..."
java -javaagent:./src/main/resources/aspectjweaver.jar -cp $CLASSPATH:$CLASSES_DIR:./src/main/resources com.aspectj.TestTarget


If we run the example we get the following:
denis@harmony:/storage/projects/java/test$ java -version
java version "1.6.0_15"
Java(TM) SE Runtime Environment (build 1.6.0_15-b03)
Java HotSpot(TM) Client VM (build 14.1-b02, mixed mode, sharing)
denis@harmony:/storage/projects/java/test$ ./load-time-weaving.sh 
Preparing the environment...
Compiling...
Running the sample...
[AppClassLoader@17590db] info AspectJ Weaver Version 1.6.5 built on Thursday Jun 18, 2009 at 03:42:32 GMT
[AppClassLoader@17590db] info register classloader sun.misc.Launcher$AppClassLoader@17590db
[AppClassLoader@17590db] info using configuration /storage/projects/java/test/src/main/resources/META-INF/aop.xml
[AppClassLoader@17590db] info register aspect com.aspectj.TestAspect
----------------------->--------- Start test -----------<--------------------- br="">TestAspect.advice() called on 'execution(void com.aspectj.TestTarget.test())'
TestTarget.test()
----------------------->--------- End test -----------<--------------------- span="">


Ant weaving


We know now how to weave by hand, lets consider using more convenient ways. The first one is a honorable ant:

build.xml
 version="1.0"?> name="aspectj-example" xmlns:aspectj="antlib:org.aspectj">

     name="src.dir" value="src/main/java"/>
     name="resource.dir" value="src/main/resources"/>
     name="target.dir" value="target"/>
     name="classes.dir" value="${target.dir}/classes"/>

     uri="antlib:org.aspectj"
            resource="org/aspectj/antlib.xml"
            classpath="${resource.dir}/aspectjtools.jar"/>

     id="aspectj.libs">
         dir="${resource.dir}"/>
    

     name="clean">
         dir="${target.dir}"/>
         dir="${target.dir}"/>
         dir="${classes.dir}"/>
    

     name="compiletime" depends="clean">
         source="1.5" srcdir="${src.dir}" classpathref="aspectj.libs" destDir="${classes.dir}"/>
         classname="com.aspectj.TestTarget" fork="true">
            
                 refid="aspectj.libs"/>
                 path="${classes.dir}"/>
            




name="postcompile" depends="clean">

message="Compiling..."/>
debug="true" srcdir="${src.dir}" classpathref="aspectj.libs" destdir="${classes.dir}"/>

message="Weaving..."/>
classpathref="aspectj.libs" inpath="${classes.dir}" aspectpath="${src.dir}" outJar="${classes.dir}/test.jar"/>

message="Running..."/>
classname="com.aspectj.TestTarget" fork="true">

refid="aspectj.libs"/>
path="${classes.dir}/test.jar"/>





name="loadtime" depends="clean">

message="Compiling..."/>
debug="true" srcdir="${src.dir}" classpathref="aspectj.libs" destdir="${classes.dir}"/>

message="Running..."/>
classname="com.aspectj.TestTarget" fork="true">
value="-javaagent:${resource.dir}/aspectjweaver.jar"/>

refid="aspectj.libs"/>
path="${classes.dir}"/>
path="${resource.dir}"/>








We can run 'ant compiletime''ant postcompile' and 'ant loadtime' and check that output is pretty much the same as the one from command-line scenarios.

Maven weaving


Finally let's consider the most convenient build tool - maven. There is a dedicated aspectj plugin that relieves the job.

pom.xml
 xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    4.0.0

com.test.aspectj
test
jar
1.0-SNAPSHOT
test
http://maven.apache.org



org.aspectj
aspectjrt
1.6.5







org.codehaus.mojo
aspectj-maven-plugin
1.2

1.5
1.5




compile






org.codehaus.mojo
exec-maven-plugin
1.1


package

java




com.aspectj.TestTarget









If we run 'mvn install' we get the following output:
denis@harmony:/storage/projects/java/test$ mvn install
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building test
[INFO]    task-segment: [install]
[INFO] ------------------------------------------------------------------------
[INFO] [aspectj:compile {execution: default}]
[INFO] [resources:resources {execution: default-resources}]
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] Copying 4 resources
[INFO] [compiler:compile {execution: default-compile}]
[INFO] Nothing to compile - all classes are up to date
[INFO] [resources:testResources {execution: default-testResources}]
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /storage/projects/java/test/src/test/resources
[INFO] [compiler:testCompile {execution: default-testCompile}]
[INFO] No sources to compile
[INFO] [surefire:test {execution: default-test}]
[INFO] No tests to run.
[INFO] [jar:jar {execution: default-jar}]
[INFO] Building jar: /storage/projects/java/test/target/test-1.0-SNAPSHOT.jar
[INFO] Preparing exec:java
[WARNING] Removing: java from forked lifecycle, to prevent recursive invocation.
[INFO] No goals needed for project - skipping
[INFO] [exec:java {execution: default}]
----------------------->--------- Start test -----------<--------------------- br="">TestAspect.advice() called on 'execution(void com.aspectj.TestTarget.test())'
TestTarget.test()
----------------------->--------- End test -----------<--------------------- br="">[INFO] [install:install {execution: default-install}]
[INFO] Installing /storage/projects/java/test/target/test-1.0-SNAPSHOT.jar to /storage/maven-repository/com/test/aspectj/test/1.0-SNAPSHOT/test-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESSFUL
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 11 seconds
[INFO] Finished at: Sat Aug 15 09:58:30 MSD 2009
[INFO] Final Memory: 12M/21M
[INFO] ------------------------------------------------------------------------


It's also easy to weave dependencies to the existing jars that are used as a project dependencies - Weaving already compiled jar artifacts

Example


I created an archive that contains the same example that I described before, so, you can simply download it and test - link. Note that the size is ~9.5 MB (because big aspectj binaries are included).

No comments: