Introduction to GluonJ
Shigeru Chiba
Table of Contents
1. Why do you use GluonJ?
2. Writing a glue
3. Refinement
4. Pointcut and Advice
1. Why do you use GluonJ?
GluonJ is a simple AOP (Aspect-Oriented Programming) system. It provides simple AOP constructs within the confines of the regular Java syntax.
There are a number of applications of AOP. For example, AOP is useful for writing test code. Suppose that we are writing the following program:
package demo; import java.math.BigDecimal; public class Bank { public BigDecimal transfer(Account src, Account dest, BigDecimal amount) { dest.deposit(amount); return src.withdraw(amount); } }
package demo; import java.math.BigDecimal; public class Account { protected BigDecimal balance; public BigDecimal deposit(BigDecimal amount) { return balance.add(amount); } public BigDecimal withdraw(BigDecimal amount) { return balance; // incomplete! } }
The definition of the Account
class is obviously not
complete. The value of balance
is never initialized
and the withdraw
method does nothing.
However, we sometimes have to write a test program for
a program including such an incomplete class. The following
test program examines the implementation of the transfer
method in the Bank
class.
package demo; import junit.framework.TestCase; import java.math.BigDecimal; public class BankTest extends TestCase { public void testTransfer() { Account mine = new Account(); Account yours = new Account(); BigDecimal pay = new BigDecimal(20000); BigDecimal balance = new BigDecimal(10000); Bank bank = new Bank(); assertEquals(balance, bank.transfer(yours, mine, pay)); } }
Of course, this test program fails if it is run.
The transfer
method calls the withdraw
method
in Account
but its implementation is not complete.
Since this failure of the test program is irrelevant to the impelementation
of the transfer
method, let us fix a problem and make the
test program succeeed. This test program should successfully finish
unless the transfer
method has a bug.
Fixing a problem is easy if you use GluonJ. What we have to do is only writing the following glue class:
package demo; import javassist.gluonj.*; import java.math.BigDecimal; @Glue public class Mock { @Refine static class Account extends Account { protected BigDecimal balance = new BigDecimal(30000); public BigDecimal withdraw(BigDecimal amount) { return balance.subtract(amount); } } }
If all the programs above are run together, the test program
BankTest
finishes successfully.
The glue class injects the initial value 30000 into
the balance
field as a dependency injection
framework does.
It also substitutes a tentative
implementation for the original one of the withdraw
method
(a complete implementation should throw an exception when the
amount
is too large).
Further details will be discussed later in this document.
Note that the glue class shown above is still useful even after
the implementation of Account
becomes complete. To perform a
unit test, the result of the test code must be independent of other
components, modules, or classes. During a test, the glue class above
makes the Bank
class independent of the implementation of the
Account
class. Even after the withdraw
method
is fully implemented, the glue class switches it back to the temporary
implementation for the unit test.
Running all the programs together is simple. If you are using the JDK 1.5 or later, give the following command-line option:
-javaagent:gluonj.jar=demo.Mock
We assume that gluonj.jar
is in the current directory.
If you are using Eclipse, this command-line option is given to
the JVM as a VM argument, which can be specified by the Launch
Configurations dialog.
2. Writing a glue
A glue is a collection of various extensions to existing classes.
It is described by using a class annotated by @Glue
(@Glue
class).
The extensions are described in the form of either a pointcut-advice or
a refinement. A @Glue
class can contain
any number of poitncut-advices and
refinements.
A pointcut-advice is a field of type Pointcut
declared in a @Glue
class. This field must be
annotated by @Before
, @After
,
or @Around
.
A refinement is a static
nested class included in a
@Glue
class. We call it a @Refine
class.
It corresponds to an intertype declaration of AspectJ.
The following is an example of @Glue
class:
package test; import javassist.gluonj.*; @Glue class Logging { @Before("{ System.out.println(`call!`); }") Pointcut pc = Pcd.call("test.Hello#say(..)"); @Refine static class Afternoon extends Hello { public String header() { return "Good afternoon, "; } } }
This @Glue
class extends the test.Hello
class shown below:
package test; public class Hello { public String header() { return "Good morning, "; } public void say(String toWhom) { System.out.println(header() + toWhom); } public static void main(String[] args) { new Hello().say("Bill"); } }
If the Hello
class is run without the @Glue
class above, then the output will be:
Good morning, Bill
On the other hand, if the Hello
class is run
with the @Glue
, then the output will be changed into:
call! Good afternoon, Bill
The @Before
pointcut-advice prints "call!"
just before the say
method is called. The @Refine
class redefines the header
method so that it will return
"Good afternoon, "
.
For more details, please see the following sections below.
Weaving
To apply a @Glue
class to other classes, we must perform
weaving, which is post-compilation program transformation. GluonJ supports
two types of weaving: off-line weaving and load-time weaving.
Ant task
The first type of weaving is off-line weaving. We use an ant task for
transforming compiled class files according to a given @Glue
class. The following is an example of build.xml
file:
<?xml version="1.0"?> <project name="hello" basedir="."> <taskdef name="weave" classname="javassist.gluonj.ant.taskdefs.Weave"> <classpath> <pathelement location="./gluonj.jar"/> </classpath> </taskdef> <target name="weave"> <weave glue="test.Logging" destdir="./out" debug="false" > <classpath> <pathelement path="./classes"/> </classpath> <fileset dir="./classes" includes="test/**/*" /> </weave> </target> </project>
This build.xml
file assumes that
gluonj.jar
is
in the current directory. It also assumes that the class files produced
by the Java compiler
are in the ./classes
directory
and they are saved in the ./out
directory after the transformation. The @Glue
class is
test.Logging
. It transforms class
files specified by the fileset
element. In the example
above, all class files in the
./classes
/test
(classes whose package names start with test
) are transformed.
The weave
task has the debug
attribute.
If it is true
, GluonJ will print detailed log messages.
This attribute is optional. The default value is false
.
Command line
Off-line weaving can be done also through command-line interface. For example,
java -jar gluonj.jar test.Logging test/Hello.class
Before running the java
command above, we must move
to the ./classes
directory.
Class files must be in the current directory.
Their path names must be ./test/Logging.class
and ./test/Hello.class
.
The second argument test.Logging
is the fully-qualified
name of a @Glue
class.
If multiple class files are transformed, they must be given as the
third, forth, ... arguments following test.Logging
.
These arguments are the path names of those class files (not class names).
If no arguments are given, gluonj.jar
prints the version
number and the copyright notices of GluonJ.
java -jar gluonj.jar
Load-time weaving
The other type of weaving is load-time weaving. We can transform
class files when the Java virtual machine (JVM) loads them.
To do that, the -javaagent
command-line option must be
given to the JVM. For example,
java -javaagent:gluonj.jar=test.Logging -cp "./classes" test.Hello
Again, we assume that gluonj.jar
is
in the current directory and class files are
in the ./classes
directory.
The @Glue
class is
test.Logging
.
Note that -javaagent:
... is a VM argument.
If we execute the command above by an ant task, the task will be:
<java fork="true" classname="test.Hello"> <classpath> <pathelement path="./classes"/> </classpath> <jvmarg value="-javaagent:./gluonj.jar=test.Logging"/> </java>
If you want to see detailed log messages, you must specify the debug
option. For example,
java -javaagent:gluonj.jar=test.Logging,debug -cp "./classes" test.Hello
Note that you must not insert an space between the comma and
debug
.
Weave according to multiple
@Glue
classes
In the case of either off-line weaving or load-time weaving,
only a single @Glue
class can be specified.
If you want to apply multiple @Glue
classes,
you must define a @Glue
class including
other @Glue
classes.
For example, the following @Glue
class includes two
other @Glue
classes:
package test; import javassist.gluonj.*; @Glue class AllGlues { @Include Logging glue0; @Include Tracing glue1; }
The @Glue
classes test.Logging
and
test.Tracing
are included in the AllGlues
as child @Glue
classes.
The @Include
specifies another @Glue
class included. If a field declaration is annotated by
@Include
, then the type of the field must be a
@Glue
class, which will be included as a child
@Glue
class.
The fields annotated by @Include
are sorted by their
names in the dictionary order and they are given a higher priority
in that order. When they are woven, a child @Glue
class
with a higher priority is woven first.
The parent @Glue
class, which contains
@Include
fields, has the lowest priority if it also contains
a refinement or a pointcut-advice as a member.
3. Refinement
@Refine
specifies a refinement on an existing class.
In other words, it extends the definition of an existing class.
Unlike the inheritance and mixin mechanisms, however, @Refine
directly modifies the existing class definition. @Refine
corresponds to the intertype declaration of AspectJ.
Suppose that the following class has been declared:
package test; public class Person { public String name; public void greet() { System.out.println("I'm " + name); } }
Let us extend this class definition for test.Person
:
@Glue class SayHello { @Refine static class Diff extends test.Person { public String message = "Hello"; public void greet() { System.out.println(message); } } }
This glue includes a static
nested class named
Diff
, which is annotated by @Refine
.
This static nested class is called a @Refine
class.
Only a static nested class can be a @Refine
class;
an inner class (i.e. non-static nested class) cannot be a
@Refine
class.
This nested @Refine
class named Diff
looks like a normal subclass of test.Person
. However, since
it is annotated by @Refine
, it directly modifies the
definition of Person
.
It appends a new field message
to the
Person
class. Also, it substitutes the
greet
method declared in Diff
for
the greet
method in the original test.Person
class.
The target class that a field and a method are appended to is
specified by the extends
keyword in the definition of
the @Refine
class.
The name of a @Refine
class has no special meaning. Other names
such as Person2
and Expand
are fine
as well as Diff
.
A subclass definition produces an extended class from its super class. The extended class and its original one coexist. On the other hand, a refinement produces an extended version of its super class (i.e. its target class) and furthermore substitutes it for the original super class. The extended class overrides the original one.
If the SayHello
glue shown above is woven,
the definition of Person
is changed by the @Refine
class to the following:
package test; public class Person { public String name; public String message = "Hello"; public void greet() { System.out.println(message); } }
Although the SayHello
glue shown above contains only one
@Refine
class, a @Glue
class can contain
any number of @Refine
classes as its member.
A @Refine
class can be declared as not a nested class
but a top-level class. The following program is equivalent to the
@Glue
class SayHello
shown above:
@Glue class SayHello { @Refine Diff refines; } public class Diff extends Person { public String message = "Hello"; public void greet() { System.out.println(message); } }
The field declaration annotated by @Refine
specifies
a refinement. The type of the declared field must be a refinement
class. This refinement class is used to extend the class
specified by extends
. In the case above,
the test.Person
class is modified according to
the Diff
class. The name of the field does not
have a special meaning. Any name can be given to it.
Add a new method
A @Refine
class can append a new method as well as
a field to its original class:
package test; public class Person { public String name; public void greet() { System.out.println("I'm " + name); } }
The following @Refine
class appends the sayHi
method to the test.Person
class.
package test; @Glue class Cheerful { @Refine static class Diff extends Person { public void sayHi(String toWhom) { System.out.println("Hi " + toWhom); } } public static class Test { public static void main(String[] args) { Person p = new Person(); ((Diff)p).sayHi("Paul"); } } }
To call the sayHi
method on a Person
object, the type of the object must be cast from Person
to Diff
. See the main
method in
the test.Cheerful.Test
class.
The sayHi
method will not be added
until the glue is woven after compilation.
At the source-code level, it is available only on the Diff
type.
After glues are woven,
the name of a @Refine
class is interpreted as being
equivalent to the name of its target class.
A method in a @Refine
class can call a method in its
target (i.e. its super class).
package test; @Glue class Cheerful2 { @Refine static class Diff extends Person { public void sayHi(String toWhom) { greet(); System.out.println("Hi " + toWhom); } public void greet() { super.greet(); System.out.println("How are you?"); } } }
super.greet()
invokes the method originally declared
in the Person
class. On the other hand, greet()
in the sayHi
method invokes the
greet
method declared in the Diff
class.
This semantics is the same as that of normal method overriding and
calls on super
.
If there are more than one @Refine
classes extending the
same class, each of them refines the target class in turn in the order
of the priority. A call on
super
reflects this semantics.
package test; @Glue class Greeting1 { @Refine static class Hi extends Person { public void greet() { System.out.println("Hi!"); super.greet(); } } } @Glue class Greeting2 { @Refine static class Wow extends Person { public void greet() { System.out.println("Wow!"); super.greet(); } } } @Glue class Greeting { @Include Greeting1 glue1; @Include Greeting2 glue2; }
If the Greeting
is woven, the two @Refine
classes Hi
and Wow
are applied in this order
to extend the Person
class. First, Hi
extends the greet
method in the Person
class.
Then, Wow
extends the greet
method that has
been modified by Hi
. Hence, super.greet()
in Wow
invokes the greet
method that
Hi
has extended.
The resulting behavior of
the greet
method is equivalent to the following one:
public void greet() { System.out.println("Wow!"); System.out.println("Hi!"); System.out.println("I'm " + name); }
Override a static
method
Unlike a regular class, a @Refine
class can
override a static
method in its super class.
For example,
package test; public class Recursive { public static int factorial(int n) { if (n == 1) return 1; else return n * factrial(n - 1); } }
The following @Refine
class overrides
the factorial
method originally declared in
the Recursive
class:
package test; @Glue class Iterative { @Refine static class Diff extends Recursive { public static int factorial(int n) { int f = 1; while (n > 1) f *= n--; return f; } } }
If this @Glue
class is woven, a call to
Recursive.factorial
computes the factorial of
a given number by using the implementation described in
the @Refine
class Diff
.
A static
method in a @Refine
class
can call the method overridden by that static
method.
package test; @Glue class Terminator { @Refine static class Diff2 extends Recursive { public static int factorial(int n) { if (n >= 1) return Recursive.factorial(n); // like a call on super. else return 0; } } }
A call to the Recursive.factorial
method invokes
the original implementation of factorial
. Recall
that, unless the caller site is within the @Refine
class
Diff2
,
a call to the Recursive.factorial
method invokes
the implementation declared in the @Refine
class
instead of the original one. This semantics has been borrowed
from method calls
through super
, which allows a method to call
an overridden method declared in its super class.
Interfaces and fields
A @Refine
class can append a field and an implemented
interface to its target class.
package test; public class Person { public String name; public void greet() { System.out.println("I'm " + name); } }
The following @Refine
class appends the three new
members to the test.Person
class.
They are the who
method,
the Nameable
interface, and
the counter
field.
package test; interface Nameable { void who(); } @Glue class WhoAreYou { @Refine static class Diff extends Person implements Nameable { public int counter = 0; public String name = "Anonym"; public void who() { counter++; greet(); } } }
The test.Nameable
interface is appended to the
interfaces that the test.Person
class implements.
The who
method included in the Nameable
interface is also appended by this @Refine
class.
The counter
field is also appended
to the Person
class. The initial value of that field
is 0
. On the other hand, the name
field is not
appended because the Person
class already includes
that field. The @Refine
class overrides
the name
field declared in
the Person
class. After the @Glue
class
above is woven, the initial value of the name
field
in Person
is set to "Anonym"
.
In the current implementation, the new initial value is assigned
to the field just after the execution of a constructor of
Person
finishes. During the execution of the constructor,
the field holds the original initial value. If the constructor calls
another constructor by this()
, the new initial value is
assigned just after the call to another constructor finishes.
A @Refine
class overridding a field in its super class
can change the initial value of the field and also append annotations to
the field. For example, the following @Refine
class
appends an annotation @Setter
to the name
field in the test.Person
class:
package test; public @inerface Setter {} @Glue class Annotater { @Refine static class Diff extends Person { @Setter public String name; } }
GluonJ allows a @Refine
interface as well as
a @Refine
class.
A @Refine
interface can append a new method to its
super interface. It can also append a new implemented interface
to its super interface.
For example, the following @Refine
interface appends
a new interface:
package test; @Glue class Annotater { @Refine static interface Diff extends Nameable, Cloneable { } }
This modifies the Nameable
interface. The second
super interface Cloneable
is appended to the
Nameable
interface as its super interface.
Note that the target of a @Refine
interface
is the first interface among
the interfaces following extends
. Thus, the second, third, ...
interfaces are appended to the first one as its super interfaces.
Constructor of a @Refine
class
Since a @Refine
class is not a regular class, the
definition of @Refine
classes must follow the
following restrictions:
No subclass of a
@Refine
class can be defined.No instance of a
@Refine
class can be made.The constructor of a
@Refine
class must be only the default constructor that takes no parameter.
If a target class does not have the default constructor that
takes no parameter, the constructor of a @Refine
class
must call non-default constructor of the target class (i.e. its super class).
However, this call is ignored when the @Refine
class
is woven. For example,
package test; public class Counter { private int counter public Counter(int c) { counter = c; } public void decrement() { if (--counter <= 0) throw new RuntimeException("Bang!"); } } @Glue class Increment { @Refine static Diff extends Counter { private int delta; public Diff() { super(0); delta = 1; } public void increment() { counter += delta; } } }
when the @Refine
class Diff
is woven,
a copy of the contents of
the constructor of Diff
is appended to the constructor
of Counter
. However, the call to the constructor of the
super class is removed from the copy. For example, super(0)
in the constructor of Diff
is not copied. The extended
constructor of Counter
by the @Refien
class
is as the following:
public Counter(int c) { counter = c; delta = 1; // copied from the @Refine class }
Accessing private members
A @Refine
class can access private methods and
fields declared in its target class
only if it is annotated by @Privileged
. This feature
is provided mainly for logging and tracing;
you should avoid using it if possible because it
violates the encapsulation property.
If a @Refine
class declares a private field with the
same name as a private field in the target class, that field is
identical to the private field in the target class. Accesses to that
field are regarded as accesses to the field in the target class.
package test; public class Person { private String name; public void setName(String newName) { name = newName; } private String getName() { return name; } public String whoAreYou() { return "I'm " + getName(); } }
package test; @Glue class PersonExtender { @Refine @Privileged static class Diff extends Person { private String name; public String whoAreYou() { return "My name is " + name; } } }
In the example above, the name
field in the Diff
class is identical to the name
field in the Person
class.
Hence, the access to name
in the whoAreYou
method
is regarded as an access to the private field name
in
the Person
.
For example, if the @Glue
class above is woven,
Person p = new Person(); p.setName("Bill"); String s = p.whoAreYou();
The value of s
is "My name is Bill"
.
It is not "I'm Bill"
or "My name is null"
.
A @Refine
class can also override the initial value
of a private field declared in its target class:
package test; @Glue class PersonExtender2 { @Refine @Privileged static class Diff2 extends Person { private String name = "Unknown"; } }
This @Refine
class changes the initial value of
the name
field declared in the Person
class.
The new value is "Unknown"
.
Overriding a private method is also allowed if a @Refine
class is privileged:
package test; @Glue class PersonExtender3 { @Refine @Privileged static class Diff3 extends Person { private String name; private String getName() { return name.toUpperCase(); } } }
If the Person
class is woven with this @Glue
class, the whoAreYou
method on a Person
object
calls the getName
method implemented in the Diff3
class.
Finally, a method in a @Refine
class can call a
private method in its target class if the @Refine
is
privileged.
package test; @Glue class PersonExtender4 { @Refine @Privileged static abstract class Diff4 extends Person { abstract String super_getName(); public String whoAreYou() { return "My name is " + super_getName(); } } }
To call a private method getName
in Person
,
an abstract method named super_getName
must be declared
in the @Refine
class. The parameter types of the
super_getName
must be the same as those of
getName
in the target class.
The method name must start with super_
, which is followed
by the name of the private method in the target class.
A call to the super_getName
method is translated into a call
to the getName
method in the target class
when the @Glue
class is woven.
A method in a @Refine
class can call a
private method overridden by that method:
package test; @Glue class PersonExtender5 { @Refine @Privileged static abstract class Diff5 extends Person { abstract String super_getName(); private String getName() { return super_getName().toUpperCase(); } } }
Note that the getName
method in the @Refine
class overrides the private method getName
in the target
class Person
. This overriding method getName
calls the super_getName
method declared in the same class.
This call is translated into a call to the getName
implemented
in the target class Person
.
Summary of @Refine
The semantics of the extension by a @Refine
class
is almost the same as the extension by a subclass. Exceptions are:
A static method can be overridden.
The initial value of a field can be overridden.
The annotations of a field can be appended.
The constructor of a
@Refine
class must be the default constructor.A
@Refine
class annotated by@Privileged
can access and override private members in its target class.
4. Pointcut and Advice
Another kind of member of a @Glue
class is
a pointcut-advice, which is a field
annotated by @Before
, @After
,
or @Around
. Unlike AspectJ, a pointcut and an advice
are not separated.
A @Glue
class can contain any number of pointcut-advices
as a @Refine
class.
A field representing a pointcut-advice, which we below call
a pointcut field, must have the type Pointcut
.
A @Glue
class can contain any number of fields of the type
Pointcut
and it can annotate only some
of them by @Before
etc. If fields of the Pointcut
type are not annotated,
then they are not treated as pointcut fields.
A typical pointcut field is like this:
@Before("{ System.out.println(`call!`); }") Pointcut pc = Pcd.call("test.Hello#say(..)");
Note that a back-quote `
used in an argument to
@Before
is interpreted as an escaped double quote
\"
. This notation is provided for convenience.
The declaration above specifies that the following block statement:
{ System.out.println("call!"); }
is executed when a say
method in test.Hello
is called. Since the annotation is @Before
, the statement
is executed just before the method body starts running, after all the
arguments to the method are evaluated. The block statement above is
below called an advice body.
A Pointcut
object specifies when a block statement
passed to @Before
is executed.
The Pcd
(Pointcut Designator) class provides factory
methods to create a Pointcut
object. The call
method returns a Pointcut
object specifies the time when
a method is called. The String
argument to call
:
test.Hello#say(..)
specifies a call to say
declared in test.Hello
.
#
is a separator between a class name and a method name.
The parameter types of the say
method are not specified
since (..)
is given. The argument above specifies calls
to say()
, say(int)
, say(String,int)
,...
Note: You might be wondering that an advice body is given as a parameter to
@Before
. In fact, other AOP systems have developers give a pointcut expression as a parameter to@Before
:@Before("call(test.Hello#say(..))") public void advice() { System.out.println(`call!`); }This is because the designers of GluonJ think an advice body should be short, normally one statement for calling on another object. A
@Glue
is not a component implementing a crosscutting concern. It is really a glue bonding such a crosscutting component a.k.a. an aspect to other components. Hence an advice body should be a short glue code just bonding them. The designers believe that this design will rescue developers from being puzzled about complicated rules to implicitly instantiate an aspect.
Pointcut designators
GluonJ can deal with several kinds of pointcuts. The following is a
list of factory methods in Pcd
and Pointcut
:
Pointcut call(String methodPattern)
When a method (or a constructor) specified bymethodPattern
is called.Pointcut get(String fieldPattern)
When the value of a field specified byfieldPattern
is obtained.Pointcut set(String fieldPattern)
When a new value is assigned to a field specified byfieldPattern
.Pointcut within(String classPattern)
When a thread of control is within a method immediately declared in the class specified byclassPattern
.Pointcut within(String methodPattern)
When a thread of control is within the body of a method specified bymethodPattern
.Pointcut annotate(String annotationPattern)
When a method or a field with an annotation specified byannotationPattern
is accessed.Pointcut when(String javaExpression)
WhilejavaExpression
istrue
. This corresponds to AspectJ'sif
pointcut designator.Pointcut cflow(String methodPattern)
While a method specified bymethodPattern
is running.
The pointcuts except call
, get
,
and set
are usually used with one of these three poitncuts
call
, get
, or set
.
Composition
These pointcuts can be composed with others by using .and
or .or
. For example,
Pointcut pc = Pcd.call("test.Hello#say(..)").and.within("test.Main");
The created Pointcut
object specifies the time when the
say
method in test.Hello
is called from
a method declared in the test.Main
class. Multiple
.and
and .or
can be used in a single expression.
For example,
Pointcut pc = Pcd.call("test.Hello#say(..)").and.within("test.Main") .or.call("test.Hello#greet());
This specifies the time when the say
method is called
within the test.Main
class or when the greet
method is called within any class.
.and
has a higher precedence than .or
.
If you want to change this precedence rule, you must use the
expr
method in Pcd
and Pointcut
.
This method works as parentheses.
Pointcut pc = Pcd.expr(Pcd.call("test.Hello#say(..)").or.call("test.Hello#greet()")) .and.within("test.Main")
This specifies the time when either say
or greet
is called within test.Main
.
The two call
pointcuts are grouped by the expr
method.
The expr
method is available in the middle of an expression:
Pointcut pc = Pcd.within("test.Main") .and.expr(Pcd.call("test.Hello#say(..)") .or.call("test.Hello#greet()"))
You can also split the declaration above into two:
Pointcut pc0 = Pcd.call("test.Hello#say(..)").or.call("test.Hello#greet()); @Before("{ System.out.println(`call!`); }") Pointcut pc = Pcd.expr(pc0).and.within("test.Main");
This also creates the same Pointcut
object. Since
the declaration of pc0
is not annotated, pc0
is not treated as a pointcut field. It is only used as a sort of
temporary variable.
For negation, GluonJ provides .not
, which can be
placed after Pcd
, .and
, or .or
.
.not
has the highest precedence.
For example,
Pointcut pc = Pcd.call("test.Hello#say(..)").and.not.within("test.Main"); Pointcut pc2 = Pcd.not.within("test.Main").and.call("test.Hello#say(..)");
Both the two pointcuts above specify the time when the say
method is called within any class except test.Main
.
Patterns
call
takes a method pattern. It is a concatenation
of a class name, a method name, and a list of parameter types.
The class name and the method name are separated by #
.
For example,
test.Point#move(int, int)
represents a move
method declared in test.Point
.
It receives two int
parameters.
The class name must be a fully qualified name, which includes a package name.
A class name and a method name can include a wild card *
.
For example, test.*
means any class in the test
package. A list of parameter types cannot include a wild card *
.
To specify any type, (..)
is used.
A method pattern may match a constructor if the method name in the pattern
is new
. For example,
test.Point#new(int, int)
This pattern matches a new
expression with two
int
arguments.
A field pattern taken by get
and set
is
similar to a method pattern. It is a concatenation of a class name and
a field name. For example,
test.Point#xpos
represents a xpos
field declared in test.Point
.
A class name and a field name can include a wild card *
as well.
Finally, a class pattern is a fully-qualified class name.
It can include a wild card *
.
An annotation pattern is a fully-qualified class name
starting with/without @
, for example,
@test.Change
. It can include a wild card *
.
Advice body
The declaration of a pointcut field is annotated by either
@Before
, @After
, or @Around
. If
@Before
is used, the advice body passed to that annotation as an
argument is executed just before the time specified by the pointcut
field. If @After
is used, the advice body is executed just
after the time specified by the pointcut field. For example, if the
pointcut field specifies the time when a method is called, then the advice
body is executed just after a return
statement in the body
of the called method is executed.
@Before(adviceBody)
The given advice body is executed before the time specified by the pointcut field annotated by this.@After(adviceBody)
The given advice body is executed after the time specified by the pointcut field annotated by this.@Around(adviceBody)
The given advice body is executed instead of the computation specified by the pointcut field annotated by this.
@Around
has a special meaning. If it is used, the
given advice body is executed instead of the code fragment specified
by the pointcut field annotated by that @Around
.
For example,
@Around("{ System.out.println(`call!`); }") Pointcut pc = Pcd.call("test.Hello#say(..)") .and.within("test.Main");
If a method in test.Main
class attempts to
invoke the say
method, that
method invocation is intercepted. Then the body of
the say
method is not executed but instead the advice
body given to the @Around
is executed. In other words,
the advice body is substituted for the call to say
from a method in test.Main
.
Runtime Contexts
An advice body is a block statement surrounded by {}
or a single statement that ends
with ;
(a semicolon).
It is written in regular Java but several special variables are
available in an advice body.
$proceed
In an advice body given to @Around
, a special form
$proceed
is available. It represents the original
computation that the advice body is substituted for.
@Around("{ System.out.println(`call!`); $_ = $proceed($$); }") Pointcut pc = Pcd.call("test.Hello#say(..)");
This advice body first prints a message and then invokes the
say
method in test.Hello
since the method
invocation is the original computation
that the advice body is substituted for.
$proceed
is used as a method name.
If it is called, it executes the original computation,
that is, the say
method.
$$
represents the original set of arguments given to the
computation, that is, the say
method.
$_
is the special variable that the resulting value
must be stored. Since @Around
substitutes the computation
specified by a pointcut field, its advice body must set $_
to an appropriate value before finishing. The type of $_
is the same type as the resulting value of the original computation.
If the return type of the originally called method is void
,
then the value stored in $_
is ignored.
Otherwise, the value stored in $_
is used as the result of
the advice body.
For example, suppose that the say
method
returns a value of the String
type:
String s = hello.say();
If the pointcut field:
@Around("{ $_ = "OK"; }") Pointcut pc = Pcd.call("test.Hello#say(..)");
is applied to the call to say
,
then the value stored in $_
, which is "OK"
,
is assigned to the variable
s
. This is because the advice body is substituted for
the call to say
. The say
method is never
executed.
The meanings of $proceed
, $$
,
and $_
,
depend on the kind of the original computation.
However, the following statement:
$_ = $proceed($$);
executes the original computation whatever is the original computation.
If the original computation is a method call, then $proceed
executes that method call. It takes the same set of parameters as the
original method and returns the same type of value.
$$
represents the list of the original arguments.
The type of $_
is the return type of the original method.
The value stored in $_
is used as the result of the advice
body.
If the original computation is a field read, $proceed
executes that operation. It takes no parameter and returns the value
of the field. $$
represents an empty list.
The type of $_
is the same as the field type.
The value stored in $_
is used as the result of the field
access instead of the value of the accessed field.
If the original computation is a field write,
$proceed
changes the value of the field.
It takes a new value as a parameter and returns no value (i.e.
void
).
$$
represents the value that the original computation
attempts to assign. $_
is still available but the value
stored in $_
is ignored.
$0
, $1
, $2
, ...
Although $$
represents a list of actual arguments
given to original computation, the value of an individual argument
is also accessible through a special variable.
If original computation is a method call, then
$1
represents the first argument, $2
represents the second argument, and so on.
$0
represents the target object that the method is
called on. To obtain a reference to the caller/accessor object,
i.e. an issuer of original computation,
this
should be used.
A value assigned to $1
, $2
, ... is
reflected on $$
. For example,
@Around("{ $1 = "Joe"; $_ = $proceed($$); }") Pointcut pc = Pcd.call("test.Hello#sayHi(String)");
After this advice body is executed,
the value stored in $_
is the value returned from
the sayHi
method called with "Joe"
.
Thus, the effects of the advice body above is equivalent to:
@Around("{ $_ = $proceed("Joe"); }") Pointcut pc = Pcd.call("test.Hello#sayHi(String)");
$0
, $1
, $2
, ... are
available within an advice body given to @Before
or @After
.
Alias
It is possible to define an alias of a special variable. For example,
@Before("{ System.out.println(msg + ` ` + callee); }") Pointcut pc = Pcd.define("msg", "$1").define("callee", "$0") .call("test.Hello#sayHi(String)");
The define
method defines an alias.
A call to define
must follow Pcd.
or
another call to define
.
In the case above, two aliases are defined. One is msg
,
which is an alias of $1
, and
the other is callee
, which is an alias of $0
.
These two aliases are available within the advice body
given to @Before
.
Logging aspect
A logging aspect is the Hello World of AOP languages. We here present an example of logging aspect.
Writing a logging aspect
in GluonJ is as simple as in other AOP languages. For example, the following
@Glue
is for printing a log message just before the
testTransfer
method in the demo.BankTest
class
calls the transfer
method in the demo.Bank
class.
We have already shown the the demo.BankTest
class and
the demo.Bank
class in Section 1.
package demo; import javassist.gluonj.*; @Glue public class Logging { @Before("{ System.out.println(`transfer: ` + $3 + ` ` + balance); }") Pointcut pc = Pcd.call("demo.Bank#transfer(..)") .and.within("demo.BankTest#testTransfer(..)"); }
call("demo.Bank#transfer(..)")
selects calls
to the transfer
method.
The selected calls are filtered out except calls by
the testTransfer
method, which is specified by
within("demo.BankTest#testTransfer(..)")
.
Thus, a log message is printed only before the transfer
method is called within the body of the testTransfer
method.
The advice body above prints the values of $3
and
balance
. $3
represents the 3rd argument
to the transfer
method called. balance
is a
local variable of the caller method testTransfer
.
Since an advice body is executed in the context of a caller-side
method, it can access a local variable and a field available in the
caller-side method, that is, the testTransfer
method
specified by within
. The BankTest
class woven with the @Glue
class above
is almost equivalent to the following normal Java program:
public class BankTest extends TestCase { public void testTransfer() { Account mine = new Account(); Account yours = new Account(); BigDecimal pay = new BigDecimal(20000); BigDecimal balance = new BigDecimal(10000); Bank bank = new Bank(); System.out.println("transfer: " + pay + " " + balance); assertEquals(balance, bank.transfer(yours, mine, pay)); } }
Copyright (C) 2006 by Shigeru Chiba. All rights reserved.