Here's a bit of Java showing the use of parameter annotations. The
printString() method has the @NonNull annotation attached to the first parameter:public class TestClass {
public static void printString(@NonNull Message message) {
System.out.println(message.getMessage());
}
public static void main(String[] argv) {
printString(new Message("Hi!"));
printString(null);
}
}
class Message {
private String text;
Message(String text) {
this.text = text;
}
public String getMessage() {
return this.text;
}
}
// Marker Annotation
@Retention(RetentionPolicy.RUNTIME)
@interface NonNull { }
The
@NonNull annotation is intended to declare a simple constraint that the method never expects to get a null passed in as the value for that parameter. Running that simple code causes (unsurprisingly) an NPE:> java TestClass
Exception in thread "main" java.lang.NullPointerException
at TestClass.printString(TestClass.java:3)
Nothing is enforcing the constraint. I want to enforce it using an aspect.
Since AspectJ 1.5.0, the pattern for matching execution of a method involving annotated types has been something like this:
execution(public void someMethod(@SomeAnnotation SomeType))
This would match "execution of a public method called
someMethod which takes only one argument of type SomeType, and that type SomeType has the annotation SomeAnnotation". Effectively this requires that SomeType was declared like this:
@SomeAnnotation
class SomeType {
...
}
For the NonNull case there is no annotation on the parameter type (
Message), it is solely on the parameter. In AspectJ 1.6.0 the syntax for the execution pointcut has been extended to allow a grouping to be specified for the annotation - to indicate whether it is expected to be an annotation associated directly with the type of the parameter or with the parameter itself.Here is the new syntax variant:
execution(public void someMethod([parameterannotations] ([typeannotations] SomeType)))
Extra parentheses in the method argument section of the pointcut determine whether a specified annotation is to be found on the type itself or the parameter. Now we can write a NonNull matching pointcut:
execution(* *(@NonNull (*)))
"execution of any method where the first parameter has the @NonNull annotation."
Contrast that with this variant (that has no parentheses):
execution(* *(@NonNull *))
"execution of any method where the type of the first parameter has the @NonNull annotation."
Or this variant:
execution(* *(@NonNull (@SomeAnnotation *)))
"execution of any method where the type of the first parameter has the annotation @SomeAnnotation, and the parameter itself has the @NonNull annotation."
Multiple annotations can be specified at either of those positions.
By extending the syntax in this way for execution() pointcuts, AspectJ avoids the need to introduce yet another top level pointcut construct for this case. It can seem like a further step into parentheses hell, it is not that difficult to understand and the pointcut signature continues to look like the method declaration when supported in this way directly in the execution pointcut syntax.
Now I can wrap the pointcut up in a Null detection aspect
public aspect NullParameterChecker {
// Note that args() is used to bind the first parameter
// so it can be checked in the advice
pointcut methodExpectingNonNullFirstArg(Object o):
execution(* *(@NonNull (*),..)) && args(o,..);
before(Object o): methodExpectingNonNullFirstArg(o) {
if (o==null) {
throw new IllegalArgumentException(
"First argument is null at "+thisJoinPoint.getSignature());
}
}
}
In both the
execution() and args() pointcut the ",.." syntax is used to indicate I do not mind if the method has more parameters, I am just interested in the first one.The simple
before() advice here will run at the start of the method and for a null argument value it will produce an IllegalArgumentException with a message of our choice. Further down the stack trace will indicate which offending caller passed null.Extending the aspect to check multiple parameters currently requires the definition of multiple pointcuts and advice:
In each case there, the '*' is used to stand in for parameters that are not of interest. There is an open AspectJ enhancement request to provide a neater syntax here, but for now I need to write the above code. Clearly it will start to get a bit 'verbose' if there are numerous parameters, but I would say if I start having methods with that 6, 7, or more, parameters - the codebase has other problems ;)
pointcut methodExpectingNonNullFirstArg():
execution(* *(@NonNull (*),..));
pointcut methodExpectingNonNullSecondArg():
execution(* *(*,@NonNull (*),..));
pointcut methodExpectingNonNullThirdArg():
execution(* *(*,*, @NonNull (*),..));
pointcut methodExpectingNonNullFourthArg():
execution(* *(*,*,*,@NonNull (*),..));
It is also possible to match on annotation parameters to constructors in a similar way (here, using 'new' instead of a method name or method name wildcard):
pointcut firstParameterNonNull():
execution(new(@NonNull (*),..));
And if I were uncomfortable with the AspectJ code style, everything shown above can be written in an annotation style pure Java aspect, using pointcuts like this:
@Pointcut("execution(* *(@NonNull (*),..)) && args(o)")
public void methodExpectingNonNullFirstArg(Object o) {}
Building the code
There are many options for linking the aspect into an existing Java code - here I'm just going to show two possibilities: source compilation and binary weaving. I'll try and cover some of the other options in a later post. I could have used AJDT (the AspectJ Eclipse support: http://eclipse.org/ajdt), but I'm a command line kind of guy. So I grabbed the latest dev build of AspectJ from http://eclipse.org/aspectj/downloads.php and installed it. Once installed and setup, I can run 'ajc' from the command line:
ajc -1.5 TestClass.java Message.java NonNull.java NullParameterChecker.aj
If I wanted to know where the aspect was affecting my code, I could add the
-showWeaveInfo option:ajc -1.5 -showWeaveInfo TestClass.java Message.java NonNull.java NullParameterChecker.aj
Join point 'method-execution(void TestClass.printString(Message))' in Type 'TestClass'
(TestClass.java:5) advised by before advice from 'NullParameterChecker'
If all source was in a single src folder, I can:
ajc -1.5 -sourceroots src -d bin
Once built, I can run it with any old JVM (1.5 or later). The compiler produces standard Java bytecode, but that bytecode will have a dependency on the aspectj runtime - a small jar that must be on the classpath when executing an AspectJ application. It provides implementations of AspectJ support functions, like the '
thisJoinPoint' construct that can be seen used in the before advice.java -classpath "bin;c:\aspectj163\lib\aspectjrt.jar" TestClassException in thread "main" java.lang.IllegalArgumentException:
First parameter is null at void TestClass.printString(Message)
AspectJ actually modifies the code using bytecode transformation (rather than source transformation). This means it does not require source code in order to apply an aspect to a codebase. Suppose my build process already creates a nicely package application.jar file and I would rather not change the compiler from javac to ajc on the front end of that process. In that case I can use binary weaving. On the command line it would look like this:
ajc -1.5 -inpath application.jar NullParameterChecker.aj -outjar applicationWoven.jar
The jar
applicationWoven.jar will contain all the classes from application.jar (modified where appropriate by the aspect) and the compiled aspect.It is even possible to build the aspect up front into a reusable aspect library. To build the
nullCheckerLibrary.jar, I can:ajc -1.5 NullParameterChecker.aj -classpath "application.jar;c:\aspectj163\lib\aspectjrt.jar"
-outjar nullCheckerLibrary.jar
Notes about this compiler invocation:
-
application.jar is on the classpath when compiling the aspect so that NonNull can be resolved (it is referenced from the pointcut)- there will be a warning message about the advice in the aspect not matching. This is fine, I am just building it into a library now, I'm not trying to weave it into a codebase.
The library can be used as input on an ajc call to link it into some codebase (using the aspectpath option). Or I could use Ant if I were adding the weaving of this aspect as a final step in my build process:
<project name="simple-project">
<taskdef
resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties">
<classpath>
<pathelement location="c:/aspectj162/lib/aspectjtools.jar"/>
</classpath>
</taskdef>
<target name="weave" >
<iajc inpath="application.jar" aspectpath="nullCheckerLibrary.jar"
outjar="applicationWoven.jar" showWeaveInfo="true">
</iajc>
</target>
</project>Now I can run just that step:
> ant weave
Buildfile: build.xml
weave:
[iajc] weaveinfo Join point 'method-execution(void TestClass.printString(Message))'
in Type 'TestClass' (TestClass.java:5) advised by before advice from 'NullParameterChecker'
BUILD SUCCESSFUL
Total time: 1 second
Running the application is the same as when it was entirely built from source, except now I specify the woven jar and the aspect library on the classpath:
java -classpath "applicationWoven.jar;nullCheckerLibrary.jar;c:\aspectj163\lib\aspectjrt.jar"
TestClass
Exception in thread "main" java.lang.IllegalArgumentException:
First parameter is null at void TestClass.printString(Message)
The aspect can even be linked to the codebase at load-time if I wanted to selectively launch the application with or without the checking aspect. However, load time weaving will be the subject of a later post.
I hope that has shown a little of the power of the latest AspectJ syntax, and the flexibility in the options available for introducing an aspect to an existing codebase. More soon...
2 comments:
nice work keep it up
that does help.
thx a lot~~
Post a Comment