(The code for this example is in
.)
InstallDir
/examples/bean
This example examines an aspect that makes Point objects into Java beans with bound properties.
Java beans are reusable software components that can be visually
manipulated in a builder tool. The requirements for an object to be
a bean are few. Beans must define a no-argument constructor and
must be either Serializable
or
Externalizable
. Any properties of the object
that are to be treated as bean properties should be indicated by
the presence of appropriate get
and
set
methods whose names are
get
property and
set
property where
property is the name of a field in the bean
class. Some bean properties, known as bound properties, fire events
whenever their values change so that any registered listeners (such
as, other beans) will be informed of those changes. Making a bound
property involves keeping a list of registered listeners, and
creating and dispatching event objects in methods that change the
property values, such as setproperty
methods.
Point
is a simple class representing points
with rectangular coordinates. Point
does not
know anything about being a bean: there are set methods for
x
and y
but they do not fire
events, and the class is not serializable. Bound is an aspect that
makes Point
a serializable class and makes
its get
and set
methods
support the bound property protocol.
The Point
class is a very simple class with
trivial getters and setters, and a simple vector offset method.
class Point { protected int x = 0; protected int y = 0; public int getX() { return x; } public int getY() { return y; } public void setRectangular(int newX, int newY) { setX(newX); setY(newY); } public void setX(int newX) { x = newX; } public void setY(int newY) { y = newY; } public void offset(int deltaX, int deltaY) { setRectangular(x + deltaX, y + deltaY); } public String toString() { return "(" + getX() + ", " + getY() + ")" ; } }
The BoundPoint
aspect is responsible for
Point
's "beanness". The first thing it does is
privately declare that each Point
has a
support
field that holds reference to an
instance of PropertyChangeSupport
.
private PropertyChangeSupport Point.support = new PropertyChangeSupport(this);
The property change support object must be constructed with a
reference to the bean for which it is providing support, so it is
initialized by passing it this
, an instance of
Point
. Since the support
field is private declared in the aspect, only the code in the
aspect can refer to it.
The aspect also declares Point
's methods for
registering and managing listeners for property change events,
which delegate the work to the property change support object:
public void Point.addPropertyChangeListener(PropertyChangeListener listener){ support.addPropertyChangeListener(listener); } public void Point.addPropertyChangeListener(String propertyName, PropertyChangeListener listener){ support.addPropertyChangeListener(propertyName, listener); } public void Point.removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { support.removePropertyChangeListener(propertyName, listener); } public void Point.removePropertyChangeListener(PropertyChangeListener listener) { support.removePropertyChangeListener(listener); } public void Point.hasListeners(String propertyName) { support.hasListeners(propertyName); }
The aspect is also responsible for making sure
Point
implements the
Serializable
interface:
declare parents: Point implements Serializable;
Implementing this interface in Java does not require any methods to
be implemented. Serialization for Point
objects is provided by the default serialization method.
The setters
pointcut picks out calls to the
Point
's set
methods: any
method whose name begins with "set
" and takes
one parameter. The around advice on setters()
stores the values of the X
and
Y
properties, calls the original
set
method and then fires the appropriate
property change event according to which set method was
called.
aspect BoundPoint { private PropertyChangeSupport Point.support = new PropertyChangeSupport(this); public void Point.addPropertyChangeListener(PropertyChangeListener listener){ support.addPropertyChangeListener(listener); } public void Point.addPropertyChangeListener(String propertyName, PropertyChangeListener listener){ support.addPropertyChangeListener(propertyName, listener); } public void Point.removePropertyChangeListener(String propertyName, PropertyChangeListener listener) { support.removePropertyChangeListener(propertyName, listener); } public void Point.removePropertyChangeListener(PropertyChangeListener listener) { support.removePropertyChangeListener(listener); } public void Point.hasListeners(String propertyName) { support.hasListeners(propertyName); } declare parents: Point implements Serializable; pointcut setter(Point p): call(void Point.set*(*)) && target(p); void around(Point p): setter(p) { String propertyName = thisJoinPointStaticPart.getSignature().getName().substring("set".length()); int oldX = p.getX(); int oldY = p.getY(); proceed(p); if (propertyName.equals("X")){ firePropertyChange(p, propertyName, oldX, p.getX()); } else { firePropertyChange(p, propertyName, oldY, p.getY()); } } void firePropertyChange(Point p, String property, double oldval, double newval) { p.support.firePropertyChange(property, new Double(oldval), new Double(newval)); } }
The test program registers itself as a property change listener to
a Point
object that it creates and then performs
simple manipulation of that point: calling its set methods and the
offset method. Then it serializes the point and writes it to a file
and then reads it back. The result of saving and restoring the
point is that a new point is created.
class Demo implements PropertyChangeListener { static final String fileName = "test.tmp"; public void propertyChange(PropertyChangeEvent e){ System.out.println("Property " + e.getPropertyName() + " changed from " + e.getOldValue() + " to " + e.getNewValue() ); } public static void main(String[] args){ Point p1 = new Point(); p1.addPropertyChangeListener(new Demo()); System.out.println("p1 =" + p1); p1.setRectangular(5,2); System.out.println("p1 =" + p1); p1.setX( 6 ); p1.setY( 3 ); System.out.println("p1 =" + p1); p1.offset(6,4); System.out.println("p1 =" + p1); save(p1, fileName); Point p2 = (Point) restore(fileName); System.out.println("Had: " + p1); System.out.println("Got: " + p2); } ... }
(The code for this example is in
.)
InstallDir
/examples/observer
This demo illustrates how the Subject/Observer design pattern can be coded with aspects.
The demo consists of the following: A colored label is a renderable object that has a color that cycles through a set of colors, and a number that records the number of cycles it has been through. A button is an action item that records when it is clicked.
With these two kinds of objects, we can build up a Subject/Observer relationship in which colored labels observe the clicks of buttons; that is, where colored labels are the observers and buttons are the subjects.
The demo is designed and implemented using the Subject/Observer design pattern. The remainder of this example explains the classes and aspects of this demo, and tells you how to run it.
The generic parts of the protocol are the interfaces
Subject
and Observer
,
and the abstract aspect
SubjectObserverProtocol
. The
Subject
interface is simple, containing
methods to add, remove, and view Observer
objects, and a method for getting data about state changes:
interface Subject { void addObserver(Observer obs); void removeObserver(Observer obs); Vector getObservers(); Object getData(); }
The Observer
interface is just as simple,
with methods to set and get Subject
objects,
and a method to call when the subject gets updated.
interface Observer { void setSubject(Subject s); Subject getSubject(); void update(); }
The SubjectObserverProtocol
aspect contains
within it all of the generic parts of the protocol, namely, how to
fire the Observer
objects' update methods
when some state changes in a subject.
abstract aspect SubjectObserverProtocol { abstract pointcut stateChanges(Subject s); after(Subject s): stateChanges(s) { for (int i = 0; i < s.getObservers().size(); i++) { ((Observer)s.getObservers().elementAt(i)).update(); } } private Vector Subject.observers = new Vector(); public void Subject.addObserver(Observer obs) { observers.addElement(obs); obs.setSubject(this); } public void Subject.removeObserver(Observer obs) { observers.removeElement(obs); obs.setSubject(null); } public Vector Subject.getObservers() { return observers; } private Subject Observer.subject = null; public void Observer.setSubject(Subject s) { subject = s; } public Subject Observer.getSubject() { return subject; } }
Note that this aspect does three things. It define an abstract
pointcut that extending aspects can override. It defines advice
that should run after the join points of the pointcut. And it
declares an inter-tpye field and two inter-type methods so that
each Observer
can hold onto its Subject
.
Button
objects extend
java.awt.Button
, and all they do is make
sure the void click()
method is called whenever
a button is clicked.
class Button extends java.awt.Button { static final Color defaultBackgroundColor = Color.gray; static final Color defaultForegroundColor = Color.black; static final String defaultText = "cycle color"; Button(Display display) { super(); setLabel(defaultText); setBackground(defaultBackgroundColor); setForeground(defaultForegroundColor); addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Button.this.click(); } }); display.addToFrame(this); } public void click() {} }
Note that this class knows nothing about being a Subject.
ColorLabel objects are labels that support the void colorCycle() method. Again, they know nothing about being an observer.
class ColorLabel extends Label { ColorLabel(Display display) { super(); display.addToFrame(this); } final static Color[] colors = {Color.red, Color.blue, Color.green, Color.magenta}; private int colorIndex = 0; private int cycleCount = 0; void colorCycle() { cycleCount++; colorIndex = (colorIndex + 1) % colors.length; setBackground(colors[colorIndex]); setText("" + cycleCount); } }
Finally, the SubjectObserverProtocolImpl
implements the subject/observer protocol, with
Button
objects as subjects and
ColorLabel
objects as observers:
package observer; import java.util.Vector; aspect SubjectObserverProtocolImpl extends SubjectObserverProtocol { declare parents: Button implements Subject; public Object Button.getData() { return this; } declare parents: ColorLabel implements Observer; public void ColorLabel.update() { colorCycle(); } pointcut stateChanges(Subject s): target(s) && call(void Button.click()); }
It does this by assuring that Button
and
ColorLabel
implement the appropriate
interfaces, declaring that they implement the methods required by
those interfaces, and providing a definition for the abstract
stateChanges
pointcut. Now, every time a
Button
is clicked, all
ColorLabel
objects observing that button
will colorCycle
.
(The code for this example is in
.)
InstallDir
/examples/telecom
This example illustrates some ways that dependent concerns can be encoded with aspects. It uses an example system comprising a simple model of telephone connections to which timing and billing features are added using aspects, where the billing feature depends upon the timing feature.
The example application is a simple simulation of a telephony system in which customers make, accept, merge and hang-up both local and long distance calls. The application architecture is in three layers.
The basic objects provide basic functionality to simulate customers, calls and connections (regular calls have one connection, conference calls have more than one).
The timing feature is concerned with timing the connections and keeping the total connection time per customer. Aspects are used to add a timer to each connection and to manage the total time per customer.
The billing feature is concerned with charging customers for the calls they make. Aspects are used to calculate a charge per connection and, upon termination of a connection, to add the charge to the appropriate customer's bill. The billing aspect builds upon the timing aspect: it uses a pointcut defined in Timing and it uses the timers that are associated with connections.
The simulation of system has three configurations: basic, timing
and billing. Programs for the three configurations are in classes
BasicSimulation
,
TimingSimulation
and
BillingSimulation
. These share a common
superclass AbstractSimulation
, which
defines the method run with the simulation itself and the method
wait used to simulate elapsed time.
The telecom simulation comprises the classes
Customer
, Call
and
the abstract class Connection
with its two
concrete subclasses Local
and
LongDistance
. Customers have a name and a
numeric area code. They also have methods for managing
calls. Simple calls are made between one customer (the caller)
and another (the receiver), a Connection
object is used to connect them. Conference calls between more
than two customers will involve more than one connection. A
customer may be involved in many calls at one time.
Customer
has methods
call
, pickup
,
hangup
and merge
for
managing calls.
public class Customer { private String name; private int areacode; private Vector calls = new Vector(); protected void removeCall(Call c){ calls.removeElement(c); } protected void addCall(Call c){ calls.addElement(c); } public Customer(String name, int areacode) { this.name = name; this.areacode = areacode; } public String toString() { return name + "(" + areacode + ")"; } public int getAreacode(){ return areacode; } public boolean localTo(Customer other){ return areacode == other.areacode; } public Call call(Customer receiver) { Call call = new Call(this, receiver); addCall(call); return call; } public void pickup(Call call) { call.pickup(); addCall(call); } public void hangup(Call call) { call.hangup(this); removeCall(call); } public void merge(Call call1, Call call2){ call1.merge(call2); removeCall(call2); } }
Calls are created with a caller and receiver who are customers. If
the caller and receiver have the same area code then the call can
be established with a Local
connection (see
below), otherwise a LongDistance
connection
is required. A call comprises a number of connections between
customers. Initially there is only the connection between the
caller and receiver but additional connections can be added if
calls are merged to form conference calls.
The class Connection
models the physical
details of establishing a connection between customers. It does
this with a simple state machine (connections are initially
PENDING
, then COMPLETED
and
finally DROPPED
). Messages are printed to the
console so that the state of connections can be
observed. Connection is an abstract class with two concrete
subclasses: Local
and
LongDistance
.
abstract class Connection { public static final int PENDING = 0; public static final int COMPLETE = 1; public static final int DROPPED = 2; Customer caller, receiver; private int state = PENDING; Connection(Customer a, Customer b) { this.caller = a; this.receiver = b; } public int getState(){ return state; } public Customer getCaller() { return caller; } public Customer getReceiver() { return receiver; } void complete() { state = COMPLETE; System.out.println("connection completed"); } void drop() { state = DROPPED; System.out.println("connection dropped"); } public boolean connects(Customer c){ return (caller == c || receiver == c); } }
The two kinds of connections supported by our simulation are
Local
and LongDistance
connections.
class Local extends Connection { Local(Customer a, Customer b) { super(a, b); System.out.println("[new local connection from " + a + " to " + b + "]"); } }
class LongDistance extends Connection { LongDistance(Customer a, Customer b) { super(a, b); System.out.println("[new long distance connection from " + a + " to " + b + "]"); } }
The source files for the basic system are listed in the file
basic.lst
. To build and run the basic system,
in a shell window, type these commands:
ajc -argfile telecom/basic.lst java telecom.BasicSimulation
The Timing
aspect keeps track of total
connection time for each Customer
by
starting and stopping a timer associated with each connection. It
uses some helper classes:
A Timer
object simply records the current
time when it is started and stopped, and returns their difference
when asked for the elapsed time. The aspect
TimerLog
(below) can be used to cause the
start and stop times to be printed to standard output.
class Timer { long startTime, stopTime; public void start() { startTime = System.currentTimeMillis(); stopTime = startTime; } public void stop() { stopTime = System.currentTimeMillis(); } public long getTime() { return stopTime - startTime; } }
The TimerLog
aspect can be included in a
build to get the timer to announce when it is started and
stopped.
public aspect TimerLog { after(Timer t): target(t) && call(* Timer.start()) { System.err.println("Timer started: " + t.startTime); } after(Timer t): target(t) && call(* Timer.stop()) { System.err.println("Timer stopped: " + t.stopTime); } }
The Timing
aspect is declares an
inter-type field totalConnectTime
for
Customer
to store the accumulated connection
time per Customer
. It also declares that
each Connection
object has a timer.
public long Customer.totalConnectTime = 0; private Timer Connection.timer = new Timer();
Two pieces of after advice ensure that the timer is started when
a connection is completed and and stopped when it is dropped. The
pointcut endTiming
is defined so that it can
be used by the Billing
aspect.
public aspect Timing { public long Customer.totalConnectTime = 0; public long getTotalConnectTime(Customer cust) { return cust.totalConnectTime; } private Timer Connection.timer = new Timer(); public Timer getTimer(Connection conn) { return conn.timer; } after (Connection c): target(c) && call(void Connection.complete()) { getTimer(c).start(); } pointcut endTiming(Connection c): target(c) && call(void Connection.drop()); after(Connection c): endTiming(c) { getTimer(c).stop(); c.getCaller().totalConnectTime += getTimer(c).getTime(); c.getReceiver().totalConnectTime += getTimer(c).getTime(); } }
The Billing system adds billing functionality to the telecom application on top of timing.
The Billing
aspect declares that each
Connection
has a payer
inter-type field to indicate who initiated the call and therefore
who is responsible to pay for it. It also declares the inter-type
method callRate
of
Connection
so that local and long distance
calls can be charged differently. The call charge must be
calculated after the timer is stopped; the after advice on pointcut
Timing.endTiming
does this, and
Billing
is declared to be more precedent
than Timing
to make sure that this advice
runs after Timing
's advice on the same join
point. Finally, it declares inter-type methods and fields for
Customer
to handle the
totalCharge
.
public aspect Billing { // precedence required to get advice on endtiming in the right order declare precedence: Billing, Timing; public static final long LOCAL_RATE = 3; public static final long LONG_DISTANCE_RATE = 10; public Customer Connection.payer; public Customer getPayer(Connection conn) { return conn.payer; } after(Customer cust) returning (Connection conn): args(cust, ..) && call(Connection+.new(..)) { conn.payer = cust; } public abstract long Connection.callRate(); public long LongDistance.callRate() { return LONG_DISTANCE_RATE; } public long Local.callRate() { return LOCAL_RATE; } after(Connection conn): Timing.endTiming(conn) { long time = Timing.aspectOf().getTimer(conn).getTime(); long rate = conn.callRate(); long cost = rate * time; getPayer(conn).addCharge(cost); } public long Customer.totalCharge = 0; public long getTotalCharge(Customer cust) { return cust.totalCharge; } public void Customer.addCharge(long charge){ totalCharge += charge; } }
Both the aspects Timing
and
Billing
contain the definition of operations
that the rest of the system may want to access. For example, when
running the simulation with one or both aspects, we want to find
out how much time each customer spent on the telephone and how big
their bill is. That information is also stored in the classes, but
they are accessed through static methods of the aspects, since the
state they refer to is private to the aspect.
Take a look at the file
TimingSimulation.java
. The most important
method of this class is the method
report(Customer)
, which is used in the method
run of the superclass
AbstractSimulation
. This method is intended
to print out the status of the customer, with respect to the
Timing
feature.
protected void report(Customer c){ Timing t = Timing.aspectOf(); System.out.println(c + " spent " + t.getTotalConnectTime(c)); }
The files timing.lst and billing.lst contain file lists for the timing and billing configurations. To build and run the application with only the timing feature, go to the directory examples and type:
ajc -argfile telecom/timing.lst java telecom.TimingSimulation
To build and run the application with the timing and billing features, go to the directory examples and type:
ajc -argfile telecom/billing.lst java telecom.BillingSimulation
There are some explicit dependencies between the aspects Billing and Timing:
Billing is declared more precedent than Timing so that Billing's after advice runs after that of Timing when they are on the same join point.
Billing uses the pointcut Timing.endTiming.
Billing needs access to the timer associated with a connection.