Weaving with AspectJ
I talked before about the most popular problems new spring users experience with spring aop (problem #1, problem #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
TestAspect.java
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:
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:
*.xml files are:
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.sh
It compiles target class and aspect class and runs target class. Following output is produced:
We can see that aspect logic is introduced to the target class.
Note: it is assumed that java remains at the path.
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:
This script compiles sources using standard javac compiler and weaves the aspects to the binary code. The output shows that aspect is correctly woven.
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:
If we run the example we get the following:
We know now how to weave by hand, lets consider using more convenient ways. The first one is a honorable ant:
build.xml
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}"/>
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
1.5
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:
Post a Comment