第三章 AspectJ實例<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
使用方面的Tracing程序
寫一個具有跟蹤能力的類是很簡單的事情:一組方法,一個控制其開或關的布爾變量,一種可選的輸出流,可能還有一些格式化輸出能力。這些都是Trace類需要的東西。當然,如果程序需要的話,Trace類也可以實現的十分的複雜。開發這樣的程序只是一方面,更重要的是如何在合適的時候調用它。在大型系統開發過程中,跟蹤程序往往影響效率,而且在正式版本中去除這些功能十分麻煩,需要修改任何包含跟蹤代碼的源碼。出於這些原因,開發人員常常使用腳本程序以便向源碼中添加或刪除跟蹤代碼。
AspectJ可以更加方便的實現跟蹤功能並克服這些缺點。Tracing可以看作是面向整個系統的關注點,因此,Tracing方面可以完全獨立在系統之外並且在不影響系統基本功能的情況下嵌入系統。
應用實例
整個例子只有四個類。應用是關於Shape的。TwoShape類是Shape類等級的基類。
public abstract class TwoDShape {
protected double x, y;
protected TwoDShape(double x, double y) {
this.x = x; this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public double distance(TwoDShape s) {
double dx = Math.abs(s.getX() - x);
double dy = Math.abs(s.getY() - y);
return Math.sqrt(dx*dx + dy*dy);
}
public abstract double perimeter();
public abstract double area();
public String toString() {
return (" @ (" + String.valueOf(x) + ", " + String.valueOf(y) + ") ");
}
}
TwoShape類有兩個子類,Circle和Square
public class Circle extends TwoDShape {
protected double r;
public Circle(double x, double y, double r) {
super(x, y); this.r = r;
}
public Circle(double x, double y) { this( x, y, 1.0); }
public Circle(double r) { this(0.0, 0.0, r); }
public Circle() { this(0.0, 0.0, 1.0); }
public double perimeter() {
return 2 * Math.PI * r;
}
public double area() {
return Math.PI * r*r;
}
public String toString() {
return ("Circle radius = " + String.valueOf(r) + super.toString());
}
}
public class Square extends TwoDShape {
protected double s; // side
public Square(double x, double y, double s) {
super(x, y); this.s = s;
}
public Square(double x, double y) { this( x, y, 1.0); }
public Square(double s) { this(0.0, 0.0, s); }
public Square() { this(0.0, 0.0, 1.0); }
public double perimeter() {
return 4 * s;
}
public double area() {
return s*s;
}
public String toString() {
return ("Square side = " + String.valueOf(s) + super.toString());
}
}
Tracing版本一
首先我們直接實現一個Trace類並不使用方面。公共接口Trace.java
public class Trace {
public static int TRACELEVEL = 0;
public static void initStream(PrintStream s) {...}
public static void traceEntry(String str) {...}
public static void traceExit(String str) {...}
}
如果我們沒有AspectJ,我們需要在所有需要跟蹤的方法或構造子中直接調用traceEntry和traceExit方法並且初試化TRACELEVEL和輸出流。以上面的例子來說,如果我們要跟蹤所有的方法調用(包括構造子)則需要40次的方法調用並且還要時刻注意沒有漏掉什麼方法,但是使用方面我們可以一致而可靠的完成。TraceMyClasses.java
aspect TraceMyClasses {
pointcut myClass(): within(TwoDShape) || within(Circle) || within(Square);
pointcut myConstructor(): myClass() && execution(new(..));
pointcut myMethod(): myClass() && execution(* *(..));
before (): myConstructor() {
Trace.traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myConstructor() {
Trace.traceExit("" + thisJoinPointStaticPart.getSignature());
}
before (): myMethod() {
Trace.traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myMethod() {
Trace.traceExit("" + thisJoinPointStaticPart.getSignature());
}
}
這個方面在合適的時候調用了跟蹤方法。根據此方面,跟蹤方法在Shape等級中每個方法或構造子的入口和出口處調用,輸出的是各個方法的簽名。因爲方法簽名是靜態信息,我們可以利用thisJoinPointStaticPart對象獲得。運行這個方面的main方法可以獲得以下輸出:
--> tracing.TwoDShape(double, double)
<-- tracing.TwoDShape(double, double)
--> tracing.Circle(double, double, double)
<-- tracing.Circle(double, double, double)
--> tracing.TwoDShape(double, double)
<-- tracing.TwoDShape(double, double)
--> tracing.Circle(double, double, double)
<-- tracing.Circle(double, double, double)
--> tracing.Circle(double)
<-- tracing.Circle(double)
--> tracing.TwoDShape(double, double)
<-- tracing.TwoDShape(double, double)
--> tracing.Square(double, double, double)
<-- tracing.Square(double, double, double)
--> tracing.Square(double, double)
<-- tracing.Square(double, double)
--> double tracing.Circle.perimeter()
<-- double tracing.Circle.perimeter()
c1.perimeter() = 12.566370614359172
--> double tracing.Circle.area()
<-- double tracing.Circle.area()
c1.area() = 12.566370614359172
--> double tracing.Square.perimeter()
<-- double tracing.Square.perimeter()
s1.perimeter() = 4.0
--> double tracing.Square.area()
<-- double tracing.Square.area()
s1.area() = 1.0
--> double tracing.TwoDShape.distance(TwoDShape)
--> double tracing.TwoDShape.getX()
<-- double tracing.TwoDShape.getX()
--> double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.distance(TwoDShape)
c2.distance(c1) = 4.242640687119285
--> double tracing.TwoDShape.distance(TwoDShape)
--> double tracing.TwoDShape.getX()
<-- double tracing.TwoDShape.getX()
--> double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.distance(TwoDShape)
s1.distance(c1) = 2.23606797749979
--> String tracing.Square.toString()
--> String tracing.TwoDShape.toString()
<-- String tracing.TwoDShape.toString()
<-- String tracing.Square.toString()
s1.toString(): Square side = 1.0 @ (1.0, 2.0)
Tracing版本二
版本二實現了可重用的tracing方面,使其不僅僅用於Shape的例子。首先定義如下的抽象方面Trace.java
abstract aspect Trace {
public static int TRACELEVEL = 2;
public static void initStream(PrintStream s) {...}
protected static void traceEntry(String str) {...}
protected static void traceExit(String str) {...}
abstract pointcut myClass();
}
爲了使用它,我們需要定義我們自己的子類。
public aspect TraceMyClasses extends Trace {
pointcut myClass(): within(TwoDShape) || within(Circle) || within(Square);
public static void main(String[] args) {
Trace.TRACELEVEL = 2;
Trace.initStream(System.err);
ExampleMain.main(args);
}
}
注意我們僅僅在類中聲明瞭一個切點,它是超類中聲明的抽象切點的具體實現。版本二的Trace類的完整實現如下
abstract aspect Trace {
// implementation part
public static int TRACELEVEL = 2;
protected static PrintStream stream = System.err;
protected static int callDepth = 0;
public static void initStream(PrintStream s) {
stream = s;
}
protected static void traceEntry(String str) {
if (TRACELEVEL == 0) return;
if (TRACELEVEL == 2) callDepth++;
printEntering(str);
}
protected static void traceExit(String str) {
if (TRACELEVEL == 0) return;
printExiting(str);
if (TRACELEVEL == 2) callDepth--;
}
private static void printEntering(String str) {
printIndent();
stream.println("--> " + str);
}
private static void printExiting(String str) {
printIndent();
stream.println("<-- " + str);
}
private static void printIndent() {
for (int i = 0; i < callDepth; i++)
stream.print(" ");
}
// protocol part
abstract pointcut myClass();
pointcut myConstructor(): myClass() && execution(new(..));
pointcut myMethod(): myClass() && execution(* *(..));
before(): myConstructor() {
traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myConstructor() {
traceExit("" + thisJoinPointStaticPart.getSignature());
}
before(): myMethod() {
traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myMethod() {
traceExit("" + thisJoinPointStaticPart.getSignature());
}
}
它與版本一的不同包括幾個部分。首先在版本一中Trace用單獨的類來實現而方面是針對特定應用實現的,而版本二則將Trace所需的方法和切點定義融合在一個抽象方面中。這樣做的結果是traceEntry和traceExit方法不需要看作是公共方法,它們將由方面內部的通知調用,客戶完全不需要知道它們的存在。這個方面的一個關鍵點是使用了抽象切點,它其實與抽象方法類似,它並不提供具體實現而是由子方面實現它。
Tracing版本三
在前一版本中,我們將traceEntry和traceExit方法隱藏在方面內部,這樣做的好處是我們可以方便的更改接口而不影響餘下的代碼。
重新考慮不使用AspectJ的程序。假設,一段時間以後,tracing的需求變了,我們需要在輸出中加入方法所屬對象的信息。至少有兩種方法實現,一是保持traceEntry和traceExit方法不變,那麼調用者有責任處理顯示對象的邏輯,代碼可能如下
Trace.traceEntry("Square.distance in " + toString());
另一種方法是增強方法的功能,添加一個參數表示對象,例如
public static void traceEntry(String str, Object obj);
public static void traceExit(String str, Object obj);
然而客戶仍然有責任傳遞正確的對象,調用代碼如下
Trace.traceEntry("Square.distance", this);
這兩種方法都需要動態改變其餘代碼,每個對traceEntry和traceExit方法的調用都需要改變。
這裏體現了方面實現的另一個好處,在版本二的實現中,我們只需要改變Trace方面內部的一小部分代碼,下面是版本三的Trace方面實現
abstract aspect Trace {
public static int TRACELEVEL = 0;
protected static PrintStream stream = null;
protected static int callDepth = 0;
public static void initStream(PrintStream s) {
stream = s;
}
protected static void traceEntry(String str, Object o) {
if (TRACELEVEL == 0) return;
if (TRACELEVEL == 2) callDepth++;
printEntering(str + ": " + o.toString());
}
protected static void traceExit(String str, Object o) {
if (TRACELEVEL == 0) return;
printExiting(str + ": " + o.toString());
if (TRACELEVEL == 2) callDepth--;
}
private static void printEntering(String str) {
printIndent();
stream.println("Entering " + str);
}
private static void printExiting(String str) {
printIndent();
stream.println("Exiting " + str);
}
private static void printIndent() {
for (int i = 0; i < callDepth; i++)
stream.print(" ");
}
abstract pointcut myClass(Object obj);
pointcut myConstructor(Object obj): myClass(obj) && execution(new(..));
pointcut myMethod(Object obj): myClass(obj) &&
execution(* *(..)) && !execution(String toString());
before(Object obj): myConstructor(obj) {
traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);
}
after(Object obj): myConstructor(obj) {
traceExit("" + thisJoinPointStaticPart.getSignature(), obj);
}
before(Object obj): myMethod(obj) {
traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);
}
after(Object obj): myMethod(obj) {
traceExit("" + thisJoinPointStaticPart.getSignature(), obj);
}
}
在此我們必須在methods切點排除toString方法的執行。問題是toString方法在通知內部調用,因此如果我們跟蹤它,我們將陷入無限循環中。這一點不明顯,所以必須在寫通知時格外注意。如果通知回調對象,通常都回存在循環的可能性。
事實上,簡單的排除連接點的執行並不夠,如果在這之中調用了其他跟蹤方法,那麼就必須提供以下限制
&& !cflow(execution(String toString()))
排除toString方法的執行以及在這之下的所有連接點。
總之,爲了實現需求的改變我們必須在Trace方面中做一些改變,包括切點說明。但是實現的改變只侷限於Trace方面內部,而如果沒有方面,則需要更改每個應用類的實現。
更多信息
參考資料
The AspectJTM Programming Guide http://www.eclipse.org/aspectj/
如果需要轉貼請寫名作者和出處。