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 { private 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("demo.Account") static class Account { private 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("test.Hello") static class Afternoon { private String header() { return "Good afternoon, "; } } }
This @Glue
class extends the test.Hello
class shown below:
package test; public class Hello { private 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 the type of a former field
in that order
has a higher priority when it is used for weaving as a @Glue
class. 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 original 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("test.Person") static class Diff { 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
.
The name of that nested class does not matter. Other names
such as P
and Expand
are also fine.
This nested @Refine
class
appends a new field message
to the
test.Person
class. Also, the
greet
method declared in the nested class substitutes
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 parameter to @Refine
. The name of the
target class must be a fully qualified name.
The resulting definition is the follow:
package test; public class Person { public String name; private String message = "Hello"; public void greet() { System.out.println(message); } }
Although the SayHello
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("test.Person") Diff refines; } public class Diff { 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 the argument to @Refine
. In the case above,
the test.Person
class is modified according to
the Diff
class. The name of the field does not
matter. Any name can be given to it.
Add a new method
A @Refine
class can append a new method to its original
class:
@Glue class Cheerful { @Refine("test.Person") static class Diff implements CheerfulPerson { public void sayHi(String toWhom) { System.out.println("Hi " + toWhom); } } } public interface CheerfulPerson { void sayHi(String toWhom); }
This @Refine
class appends the sayHi
method to the test.Person
class.
Since it implements the CheerfulPerson
interface,
the test.Person
class is extended to implement
the CheerfulPerson
interface.
Implementing CheerfulPerson
is necessary to call the
added method sayHi
:
Person p = new Person(); ((CheerfulPerson)p).sayHi("Stieve");
Since the original definition of the Person
class
does not include the sayHi
method, the code fragment
above cannot be compiled without a type cast to CheerfulPerson
.
Note that the sayHi
method will not be added
until the glue is woven after compilation.
If an added method is static
, the technique above
does not work. A different technique must be used.
@Glue class Cheerful2 { @Refine("test.Person") static class NewPerson { public static void sayHi(String toWhom) { System.out.println("Hi " + toWhom); } } @Refine("test.Skit") static class NewSkit { public static void main(String[] args) { NewPerson.sayHi("Paul"); } } }
Note that the main
method in NewSkit
calls a static
method declared in NewPerson
,
which is not an original class but a @Refine
class.
However, this call is interpreted as a call to the sayHi
method
added to the test.Person
class.
To weave a glue, GluonJ copies a method in a @Refine
class to its original class. When copying, GluonJ substitutes
original class names for all occurrences of @Refine
class
names in the method definition. In the case above, GluonJ substitutes
test.Person
for NewPerson
when it copies the
main
method from NewSkit
to test.Skit
.
The class names substituted for are the names of all @Refine
classes belonging to the same @Glue
class.
Accesses to a field in a refined class
A method in a @Refine
class can access
fields in its target class. For example,
@Glue class Setter { @Refine("test.Person") static class Diff implements PersonSetter { public String name; public void setName(String newName) { name = newName; } } } public interface PersonSetter { void setName(String newName); }
The above @Refine
class adds a setName
method to the test.Person
class.
The name
field declared in
the @Refine
class represents the same name
field in the test.Person
class.
If a @Refine
class and the refined
class include fields with the same name, those fields are
identical. In the case above, the resulting class is:
package test; public class Person implements PersonSetter { public String name; public void greet() { System.out.println("I'm " + name); } public void setName(String newName) { name = newName; } }
Since the above @Refine
class implements the
PersonSetter
interface, the resulting class
also implements that interface.
If a field in a @Refine
class has an initial value,
that initial value overrides the initial value of the corresponding
field in the refined class. For example,
@Glue class Init { @Refine("test.Person") static class Diff { public String name = "Joe"; } }
After weaving this glue, the initial value of the name
field in test.Person
becomes "Joe"
.
Accesses to a method in a refined class
A method in a @Refine
class can access
methods in its refined class.
If a @Refine
class declares an abstract
method
with the same name as a method in its refined class, that
abstract
method represents the corresponding method in the
refined class.
@Glue class Sociable { @Refine("test.Person") static abstract class Diff implements CheerfulPerson { public abstract void greet(); public void sayHi(String toWhom) { System.out.println("Hi " + whom); greet(); } } }
The sayHi
method calls the greet
method
declared in the refined class. Note that the @Refine
class declares an abstract
method named greet
.
Furthermore, the @Refine
class is also abstract
since only an abstract
class can declare an
abstract
method in Java.
If an abstract
method in an @Refine
class
has a name starting with orig_
, it has a special meaning.
@Glue class Greeting { @Refine("test.Person") static abstract class Diff { public abstract void orig_greet(); public void greet() { System.out.println("Hello"); orig_greet(); } } }
This @Refine
class overrides the greet
method in the refined class. However, the overridden greet
method is called from the overriding method by
the orig_greet
method. The abstract
method
orig_greet
represents the original greet
method declared in the refined class.
Declaring an abstract
orig_
... method enables a call
to an overridden method. However, this technique does not work for
calling an overridden static
method since a static
method cannot be abstract
in Java.
As for a static
method, we must use a different technique.
For example,
package test; public class Greeting { public static String say(Person p) { return "Hi, " + p.name; } }
Let us modify the say
method:
@Glue class Verbose { @Refine("test.Greeting") static class Diff { @Abstract public static String orig_say(Person p) { return null; } public static String say(Person p) { return orig_say(p) + ". How are you?"; } } }
The overridden say
method is called by orig_say
.
If p.name
is "Bill"
, then the overriding
say
method returns "Hi, Bill. How are you?"
.
The declaration of a static
orig_
... method
must be annotated with @Abstract
. Since the body of that method
is ignored, it can be any statement if it is valid in Java. Normally,
it is a return
statement that returns null
or
0
. If the return type is void
, then the method
body can be empty.
Using this
When the special variable this
is used in a method in a @Refine
class, you must consider its type:
import test.Person; import static javassist.gluonj.Gluon.$refine; @Glue class UseThis { @Refine("test.Person") static class Diff { public Person self() { return (Person)$refine(this); } } }
Since the type of this
is not Person
but Diff
, the self
method must perform
type cast. GluonJ provides a static
method named
$refine
for this purpose.
The cast expression for this
should take this form:
( <target type> )$refine(this)
Summary of @Refine
In summary, each member in a @Refine
class
is copied to its target class as following:
A field in a
@Refine
class:is appended to the refined class
if it has a different name from the other fields in the refined class. Here, the other fields include one inherited from a super class.is merged with a field in the refined class
if they have the same name and type.If the field in the
@Refine
class has an initial value,
that initial value overwrites the initial value of the field in the refined class.Since the new initial value is assigned just before a constructor finishes, an original initial value might be used during the execution of the constructor body.
The annotations of the field is also merged.
A method in a
@Refine
class:is appended to the refined class
if it has a different name from the other methods in the refined class.The appended method can be called through an interface, which must be also appended by the
@Refine
class.If the method in the
@Refine
class isabstract
and it has a name starting withorig_
, such asorig_XXX
, then it refers to the method namedXXX
in the refined class.
is merged with a method in the refined class
if they have the same name and signature.The method in the
@Refine
class overrides the method in the refined class
unless the former method isabstract
.Otherwise, if the method in the
@Refine
class isabstract
, then it is absorbed in the method in the refined class.
If you add a new method, you must also add an interface for accessing
that added method. An interface implemented by a @Refine
class is added to the refined class.
- Interfaces implemented by a
@Refine
classare also implemented by the refined class (if not implemented yet). The refined class does not inherit from the super class of the
@Refine
class.
@Refine
can be also used for extending an interface.
If a target is an interface, a @Refine
class must be also
an interface type. The rule of copying methods is:
- A method in a
@Refine
interfaceis copied to the refined interface (if it is not declared in the refined interface).
An interface that a @Refine
interface extends is
added to the list of the super interfaces of the refined interface.
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.