アプリケーションに特有なポイントカット指定子
ポイントカットの宣言は、しばしば脆く、壊れやすいことが知られています。開発者が織り込み対象のクラス定義を修正・変更しようとした場合、そのクラスのジョインポイントを選択しているポイントカットが、壊れてしまうケースがよくあります (fragile pointcut problem)。例えば、ポイントカット call("Rectangle#setX(..)")
は、Rectangle
クラスに宣言された setX
メソッドの呼び出しをジョインポイントとして指定します。もしリファクタリングの影響などにより、この Rectangle
クラスの setX
メソッドの名前が修正・変更されたとすると、このポイントカットは指定したジョインポイントを選択できなくなってしまいます。
この問題に対し、何人かの研究者は、プログラムのロジックを利用する手法でジョインポイントを指定することを提案しています。私たちもまた、Josh (ジョッシュ) で、そのアイディアを提案しています。Josh は、以前私たちが開発した強力なポイントカット記述力をもつ AOP 言語です。Josh では、利用者が Java で書かれた Josh のプラグインとして、新しいポイントカット指定子を実装することができます。Josh は Java バイトコード変換器 Javassist (www.javassist.org) の上で実装されているため、新しいポイントカット指定子を実装するために、利用者は Javassist が提供している全ての機能を使うことができます。複雑なプログラムのロジックに従ったジョインポイントを指定するためのポイントカット指定子を、実装することが可能になります。このことは、GluonJ も同様です。GluonJ も、Javassist の上で実装されています。もし利用者が Javassist を利用したプログラミングを熟知していれば、複雑なプログラム・ロジックを基にしたポイントカット指定子を、簡単に実装することができます。
updater
ポイントカット指定子
以下では、どのように updater
ポイントカット指定子を GluonJ に実装するのかを示します。updater
ポイントカットは、AOSD 2004 で発表された Josh に関する論文の第 4 章で説明されている例です (この例は AOSD 論文の発展版である日本語論文でも説明されています)。
updater
ポイントカット指定子は、クラス名とメソッド名の 2 つの引数をとります。ここでは、そのクラスの名前は Shape
であり、メソッドの名前は redraw
だとします。Shape
もしくはその Shape
のサブクラスに宣言されている redraw
メソッドの役割は、そのクラスが表現する図形オブジェクトをスクリーン上に描くことです。そのため、redraw
メソッドは、そのクラスに宣言されたいくつかのフィールド値を読み出します。例えば、Shape
のサブクラスである Line
クラスに宣言されている redraw
は、フィールド x0
, y0
, x1
, そして y1
の値を読み出します。これらのフィールドの値は、スクリーン上に描かれる Line
オブジェクトの場所を決定するために使われます。updater
ポイントカット指定子は、これらのフィールドの値を更新したメソッドの呼び出しを指定します。そのような値の更新は、図形オブジェクトの形状 (もしくはスクリーン上の位置) を変更します。それゆえ、updater
によって指定されたジョインポイントは、図形オブジェクトの形状を変更するメソッドの呼び出しなのです。
updater
ポイントカット指定子の実装
以下に、ユーティリティ・クラス javassist.gluonj.util.SimplePcNode
を利用した、updater
ポイントカット指定子の実装方法を示します。その実装は単純です (ここでは、updater
ポイントカット指定子のソースコードの一部を記載します。完全なソースコードは、javassist/gluonj/plugin/Updater.java
です)。
package javassist.gluonj.plugin; import javassist.*; // 他の import 文を省略します @Glue public class Updater { @Refine static class Pcd2 extends Pcd { public static Pointcut updater(String className, String methodName) { Pointcut pc = make(); return ((Pcut2)pc).updater(className, methodName); } } @Refine static class Pcut2 extends Pointcut { public Pcut2() { super(null); } public Pointcut updater(String className, String methodName) { setTree(new UpdaterPc(className, methodName)); return this; } } public static class UpdaterPc extends SimplePcNode { // 後で改めて示します。 } }
@Glue
クラス Updater
の基本的な構成は、MetaTag
の場合と同じです。木のノードを表現する UpdaterPc
クラスの定義は、以下のようになります。
public static class UpdaterPc extends SimplePcNode { private ClassPool cpool; private String rootClassName; private CtClass rootClass; private String methodName; private HashMap updatersMap; public UpdaterPc(String cName, String mName) { rootClassName = cName; methodName = mName; updatersMap = new HashMap(); } /** * アドバイスの織り込みを始める前に呼び出されます。 */ public void prepare(PatternParser p) throws WeaveException { cpool = p.getClassPool(); try { rootClass = cpool.get(rootClassName); } catch (NotFoundException e) { throw new WeaveException(e); } } /** * 与えられたクラスに宣言されている updater メソッドを収集します。 */ public void inspect(CtClass clazz) throws WeaveException { if (!clazz.subclassOf(rootClass)) return; try { String className = clazz.getName(); CtMethod m = clazz.getDeclaredMethod(methodName); HashMap fields = accessedFields(className, m); HashMap updaters = updateMethods(clazz, fields); updatersMap.put(className, updaters); } catch (CannotCompileException e) { throw new WeaveException(e); } catch (NotFoundException e) { return; } } /** * 与えられたジョインポイントが、updater メソッドの呼び出しであれば、 * true を返します。 */ public boolean match(MethodCall joinPoint, Residue[] residue) throws WeaveException { String className = joinPoint.getClassName(); Object updaters = updatersMap.get(className); if (updaters == null) return false; try { return ((HashMap)updaters).get(joinPoint.getMethod()) != null; } catch (NotFoundException e) { throw new WeaveException(e); } } // 以下、省略します } }
このクラス内には、重要なメソッドが 3 つあります。それは prepare
, inspect
, そして match
メソッドです。prepare
は、GluonJ がアドバイスを織り込み始める前に呼び出されるメソッドです。主に、織り込みのための準備作業を行います。PatternParser
オブジェクトは、クラス・パターンやメソッド・パターンを解析するために使われます。
GluonJ がそれぞれのクラスにアドバイスを編み込むたびに、inspect
メソッドは呼び出されます。今回の例では、inspect
メソッド内にて、与えられたクラスの定義をチェックし、そのクラスに定義されている updater メソッドを収集します。収集された updater メソッドのリストは、後で利用するため updatersMap
フィールドに登録しておきます。
match
メソッドは、ジョインポイントが見つかったときに呼び出されます。ここでは、match
メソッドの引数として与えられたメソッドの呼び出しが、上記の inspect
メソッドによって収集された updater メソッドへの呼び出しであるかどうかをチェックします。もし match
メソッドの返り値が true
(正確には、ポイントカット式全体の値が true
) であれば、GluonJ はそのジョインポイントにアドバイスを織り込みます (アドバイス・ボディをインライン展開します)。もし false
である場合、GluonJ はそのジョインポイントの箇所では何もしません。
match
メソッドは、UpdaterPc
クラスのスーパークラスである SimplePcNode
から継承されたメソッドです。SimplePcNode
には、引数の型が異なる複数の match
メソッドがあります。もしこの updater
ポイントカット指定子が異なるジョインポイントの型 (フィールド・アクセスなど) を指定する場合、他の match
メソッド (match(FieldAccess, Residue[])
) をオーバーライドしなければなりません。
updater
ポイントカット指定子の使い方
ここでは、updater
ポイントカット指定子の使い方を説明していきます。もちろん、上記に示した @Glue
クラスを、事前に GluonJ へインストールしておかなければなりません (インストールの方法は、@Glue
クラスをインストールするを参考にしてください)。
以下は、@Glue
クラスを編み込む対象となるサンプルプログラムです。
package test; import java.util.Iterator; import java.util.LinkedList; import java.util.List; public class ShapeEditor { private List shapes; public ShapeEditor() { shapes = new LinkedList(); } public void addShape(Shape shape) { shapes.add(shape); } public void paint() { Iterator iter = shapes.iterator(); while (iter.hasNext()) ((Shape)iter.next()).redraw(); } public static void main(final String[] args) { new ShapeEditor().run(); } public void run() { Point p = new Point(this, 11, 17); p.move(5, 4); p.setName("p1"); } } abstract class Shape { protected ShapeEditor editor; public Shape(ShapeEditor e) { editor = e; e.addShape(this); } abstract void redraw(); } class Point extends Shape { private int x; private int y; private String name; // redraw メソッド内でアクセスされません public Point(ShapeEditor e, int x, int y) { super(e); this.x = x; this.y = y; } public int getX() { return x; } public void setX(int newX) { x = newX; } public int getY() { return y; } public void setY(int newY) { y = newY; } public String getName() { return name; } public void setName(String s) { System.out.println("setName()"); name = s; } public void move(int dx, int dy) { System.out.println("move()"); x += dx; y += dy; } public void redraw() { System.out.println("(" + x + ", " + y + ")"); } }
Point
クラス内で宣言された redraw
メソッドは、そのオブジェクトをスクリーン上に描画するため、フィールド x
と y
の値を読み込みます。しかし、フィールド name
の値を読むことはありません。それゆえ、もし以下のような @Glue
クラスを定義した場合、GluonJ は x
もしくは y
の値を更新するメソッドの呼び出しを、ジョインポイントとして指定します。
package test; import javassist.gluonj.*; @Glue public class UpdateScreen { @After("{ paint(); }") Pointcut pc = Pcd.updater("test.Shape", "redraw"); }
ポイントカット pc
は、setX
, setY
, もしくは move
メソッドの呼び出しを指定します。setName
メソッドの呼び出しは、選択されません。ShapeEditor
クラスの main
メソッドが実行されたとき、move
メソッドの呼び出し箇所でのみ、@After
アドバイスのボディが実行されます。ただし、@After
アドバイスは、move
メソッドの呼び出し側で実行されます。この例の場合、ShapeEditor
クラスの run
メソッドの内で実行されます。そのため、@After
アドバイス・ボディのスコープ内の this
オブジェクトは、ShapeEditor
オブジェクトであり、paint
メソッドは、ShapeEditor
オブジェクトの paint
メソッドを表します。
Copyright (C) 2006 by Shigeru Chiba and Muga Nishizawa. All rights reserved.