在上一篇部落格初窺AspectJ中,我們提到AspectJ給java提供了三種新的結構,pointcut,advice以及inter-type declaration(ITD),而且我們通過一個簡單的Demo介紹了如何使用pointcut和advice。而本文将介紹inter-type declaration是什麼,可以做什麼,最後同樣會通過一個Demo來介紹如何使用。後文将主要用ITD來表示inter-type declaration。
本文中Demo的代碼可以在github aspect-demo中找到。
ITD與成員注入
inter-type declaration (ITD),翻譯成中文是類型間聲明。即使看到中文翻譯,相信大家還是一頭霧水,不知所雲,是以我不是很喜歡對一些英文名字,尤其是技術名字進行生硬的翻譯,這隻會增加大家的了解負擔。其實,換一種說法可能更好了解,member introduction(成員注入),其目的就是通過aspect的方式,在現有的類中注入一些新的成員變量或者成員方法。通過aspect,我們可以向一個類中注入如下成員:
- 成員變量(final或者非final)
- 方法
- 構造函數
除了往類裡面添加内容,aspect還可以修改java中的interface(接口),實作在現有接口中注入:
- 方法的預設實作
- 非final的域
通過ITD注入的成員的通路修飾符可以是:
- private: 通過private聲明的私有成員屬于目标類,但是呢,隻對aspect腳本可見,而對目标類不可見;
- public: 聲明為public的成員對所有類和apsect都可見;
- default package protected:這裡的包可見性是相對于aspect所在的包,而不是相對于目标類所在的包。
inter-type declaration示例
在編寫aspect之前,先準備一個簡單的java類:
package cc.databus.aspect.intertype;
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
}
有了這個基礎類,下面來看看如何通過aspect修改這個類實作的接口,成員變量以及成員方法。這裡是我們的aspect代碼:
package cc.databus.aspect.intertype;
public aspect PointAspect {
// creates a new interface named HasName
private interface HasName{}
// make class Ppint implements HashName
declare parents: Point implements HasName;
// make HasName has a field named name
private String HasName.name;
// make HasName has a method getName() and default implemented
public String HasName.getName() {
return name;
}
// make HasName has a method named setName and default
public void HasName.setName(String name) {
this.name = name;
}
// add a field named created to class Point
// with default value 0
long Point.created = 0;
// add a field named lastUpdated to class Point
// with default value 0
private long Point.lastUpdated = 0;
// add a private method setUpdated()
private void Point.setUpdated() {
this.lastUpdated = System.currentTimeMillis();
}
// implement toString() for Point
// include the fields added in the aspect file
public String Point.toString() {
return String.format(
"Point: {name=%s, x=%d; y=%d, created=%d, updated=%d}",
getName(), getX(), getY(), created, lastUpdated);
}
// pointcut the constructor, and set the value for created
after() returning(Point p) : call(Point.new(..)) && !within(PointAspect) {
System.out.println(thisJoinPointStaticPart);
System.out.println("Set created");
p.created = System.currentTimeMillis();
}
// define a pointcut for setX and setY
pointcut update(Point p): target(p) && call(void Point.set*(..));
// make the lastUpdated updated every time
// setX or setY invoked
after(Point p): update(p) && !within(PointAspect) {
System.out.println("set updated for Point due to " + thisJoinPointStaticPart);
p.setUpdated();
}
}
在上面的aspect檔案中,我們首先定義了一個接口,并且讓Point類實作該接口,且給該新接口加了一個成員變量(name)并實作了對應的setter/getter:
// creates a new interface named HasName
private interface HasName{}
// make class Ppint implements HashName
declare parents: Point implements HasName;
// make HasName has a field named name
private String HasName.name;
// make HasName has a method getName() and default implemented
public String HasName.getName() {
return name;
}
// make HasName has a method named setName and default
public void HasName.setName(String name) {
this.name = name;
}
随後,我們給Point類加了兩個成員變量,并實作了兩個成員方法。其中,實作toString()接口的時候,我們把通過aspect注入的成員變量也都包含在結果裡面:
// add a field named created to class Point
// with default value 0
long Point.created = 0;
// add a field named lastUpdated to class Point
// with default value 0
private long Point.lastUpdated = 0;
// add a private method setUpdated()
private void Point.updated() {
this.lastUpdated = System.currentTimeMillis();
}
// implement toString() for Point
// include the fields added in the aspect file
public String Point.toString() {
return String.format(
"Point: {name=%s, x=%d; y=%d, created=%d, updated=%d}",
getName(), getX(), getY(), created, lastUpdated);
}
最後,我們加了兩個pointcut一級advice,分别實作在調用Point構造函數之後為created的指派,以及調用setX(int), set(int)以及setName(string)的時候更新lastUpdated成員變量(這裡使用!within(PointAspect)排除掉在aspect腳本裡面調用set*的情況):
// pointcut the constructor, and set the value for created
after() returning(Point p) : call(Point.new(..)) && !within(PointAspect) {
System.out.println(thisJoinPointStaticPart);
System.out.println("Set created");
p.created = System.currentTimeMillis();
}
// define a pointcut for setX and setY
pointcut update(Point p): target(p) && call(void Point.set*(..));
// make the lastUpdated updated every time
// setX or setY invoked
after(Point p): update(p) && !within(PointAspect) {
System.out.println("set updated for Point due to " + thisJoinPointStaticPart);
p.setUpdated();
}
同樣,我們可以建立一個單元測試類來進行測試:
package cc.databus.aspect.intertype;
import org.junit.Test;
public class TestPointAspect {
@Test
public void test() {
Point point = new Point(1,1);
point.setName("test");
point.setX(12);
point.setY(123);
System.out.println(point);
}
}
運作測試,我們能看到如下結果:
call(cc.databus.aspect.intertype.Point(int, int))
Set created
set updated for Point due to call(void cc.databus.aspect.intertype.Point.setName(String))
set updated for Point due to call(void cc.databus.aspect.intertype.Point.setX(int))
set updated for Point due to call(void cc.databus.aspect.intertype.Point.setY(int))
Point: {name=test, x=12; y=123, created=1536153649547, updated=1536153649548}
可以看到,通過aspect注入的成員對象和成員方法都是工作的。
總結
ITD着實是一個強大的功能,能夠友善給現有類注入新的功能。但是,筆者認為使用這種方法相對容易出錯,尤其在大項目的情況下,如果通過大量的aspect腳本來實作功能,相信對後期的維護是一個很大的挑戰。是以,我建議在沒有spring這種架構做支撐的情況下,不要大量的使用這種方法為項目造血。