ラベル AspectJ の投稿を表示しています。 すべての投稿を表示
ラベル AspectJ の投稿を表示しています。 すべての投稿を表示

2009年12月23日水曜日

Nested Set Model と Java のマッピング例

前ポストの続きで、RDB の Nested Set Model と POJO との対応を考えてみる。 Nested Set モデルに従って電化製品の種別を表す、下のようなテーブル `category`を、クラス Category にマッピングするとして、、、
+-------------+----------------------+------+------+
| category_id | name                 | lft  | rgt  |
+-------------+----------------------+------+------+
|           1 | ELECTRONICS          |    1 |   10 |
|           2 | TELEVISIONS          |    2 |    9 |
|           3 | TUBE                 |    3 |    4 |
|           4 | LCD                  |    5 |    6 |
|           5 | PLASMA               |    7 |    8 |
+-------------+----------------------+------+------+
こんな風に書いて、、、
public static void main(String[] args) {
  ・・・ 略
  EntityManager em = emf.createEntityManager();
  Category node = em.find(Category.class, 4);
  someBusinessLogic(node);
  ・・・ 略
}

static void someBusinessLogic(Category node) {
  for (Category a: node.ancestors()) {
     System.out.printf("%s %s%n", a.getId(), a.getName());
  }
}
、、、以下のような出力を得たい。
1 ELECTRONICS
2 TELEVISIONS
someBusinessLogic() では EntityManager を触っていない事に留意しつつ、どんな風に書けるか考えてみる。(話を簡単にするため、Nested Set への操作は祖先ノードを検索する ancestors() メソッドのみとする。) ■ 実装例 ◆エンティティクラス まず、RDBのテーブルにマッピングされる Category エンティティはこんな風にした。
@Entity(name="category")
public class Category {
   static aspect UseNestedSet extends NestedSetIdiom<Category>{}

   @Id()
   @Column(name="category_id")
   private int id;
   private String name;

   @Transient
   NestedSet<Category> nestedSet;

   ・・・id と name のゲッタセッタ

   public List<Category> ancestors() {
      return nestedSet.ancestors();
   }
}
ここで、フィールド nestedSet は Category を要素として持つ Nested Set への ファサード・オブジェクトで、以下のインターフェイスを持つ。
public interface NestedSet<T> {
   List<T> ancestors();
}
interface 定義の中では、Nested Set というデータ集合への操作を契約として表現するのみで、JPA など下位レイヤのAPIにはタッチしない。 さて、実装クラスについては、次のことが問題になる。
  1. いつ、どのようにして NestedSet インスタンスと Category を関連付けるか。
  2. 実際の DB へのアクセスは、NestedSet 実装クラスが受け持つ事になるが、そこで用いる EntityMangaer をどう関連付けるか。
これらの問題を解決するために、AspectJ を使ってみた。上記 Category クラス定義冒頭の内部アスペクトはそのためのもので、抽象アスペクト NestedSetIdiom を具象化している。 ◆アスペクトコード 抽象アスペクト NestedSetIdiom は以下のようなもので、NestedSetUtil.injectNestedSet() を使って、NestedSetImpl、EntityManager、および T の間の関連付けを行う(この例では T=Category )。
public abstract aspect NestedSetIdiom<T> 
      extends EntityCreationWormhole<T> {

   protected void postEntityCreated(EntityManager em, T entity) {
      NestedSetUtil.injectNestedSet(em, entity);
   }
}
class NestedSetUtil {
   public static <T> void injectNestedSet(EntityManager em, T entity) {
      try {
         for (Field f: entity.getClass().getDeclaredFields()) {
            if (NestedSet.class.equals(f.getType())) {
               f.set(entity, new NestedSetImpl<T>(em, entity));
               return;
            }
         }
      } catch (Exception e) ・・・略
   }
   ・・・略: その他メソッド
}
postEntityCreated() は、
  • 基底抽象アスペクト EntityCreationWormhole で定義されたタイミングで呼ばれ、
  • Wormhole を通じて渡されてきた EntityManager で NestedSet 実装クラスを初期化し、
  • これを T entity のフィールドとして挿し込む。
アスペクト EntityCreationWormhole の定義は以下のようになる。
public abstract aspect EntityCreationWormhole<T> {
   pointcut caller(EntityManager em): 
      call(public * EntityManager.*(..)) && target(em);
   pointcut callee(): execution(T+.new(..));
   pointcut wormhole(EntityManager em) : cflow(caller(em)) && callee();

   after(EntityManager em, T entity): wormhole(em) && this(entity) {
      postEntityCreated(em, entity);
   }
   protected abstract void postEntityCreated(EntityManager em, T entity);
}
これで上記の問題1と2が解決できた。あとは NestedSetImpl の ancestors()実装で、EntityManager を使った先祖 Category 検索コードを書くだけ。 ◆ NestedSet 実装クラス これは適当にどんな書き方でも良いが、例えば以下のように書ける。
class NestedSetImpl<T> implements NestedSet<T> {
   public static final String SELECT_ANCESTORS = 
      "select p.* from %1$s as p, %1$s as c " +
      " where c.%2$s = ?1 " +
      "  and p.lft < c.lft and c.rgt < p.rgt";

   private final T entity;
   private final EntityManager entityManager;

   NestedSetImpl(EntityManager em, T entity) {
      this.entityManager = em;
      this.entity = entity;
   }
   public List<T> ancestors() {
      String query = String.format(
         SELECT_ANCESTORS, 
         NestedSetUtil.tableNameOf(entity), 
         NestedSetUtil.idColNameOf(entity));
      return (List)entityManager.createNativeQuery(query, entity.getClass())
         .setParameter(1, NestedSetUtil.idOf(entity))
         .getResultList();
   }
}
ここで使っている NestedSetUtil の tableNameOf(), idColNameOf(), getIdOf() はベタなリフレクションコードなので割愛するが、以下のような仕様になる。
  • tableNameOf(T entity): T についた@Entity のname属性を返す。
  • idColNameOf(T entity): T のフィールドのうち、型 int で @Id アノテーションがついたものを見つけて、その @Column の name属性を返す。
  • idOf(T entity): T のフィールドのうち、型int で@Id アノテーションがついたものを見つけて、その値を返す。
とりあえず上記のようなコーディングで、JPA コードを排除した Java ビジネスロジックコードから、Nested Set Model 準拠 テーブルにある情報にアクセスできるようになる。 Nested Set への他の検索操作(例えば子孫要素の一括取得など)も NestedSet で宣言してNestedSetImplで実装する形で追加できる。 ■ 制約と課題 上述の解は、Proof-of-concept 目的のサンプルコードで、いくつか制約がある。また AspectJ や Nested Set Model に関連する本質的な課題もある。 以下、単純にコードを書き足すだけでどうにでもできそうな制約。
  • NativeQuery を作る際、@Entityや@Columnで表名・列名が明示されていることに依存しているが、本来はクラス名とフィールド名からデフォルト名を得ることもできるようにすべき。
  • ネイティブ SQL Where句中の、自分の位置を決めるため等式 c.%2$s = ?1で、単一の int 列を id として決め打ちしているが、JPA でIDとして許容されるものは複合キーでもその他の型でも、なるべく対応できたほうがいい。
以下は、ちょっと研究が必要な課題。
  • ancestor で集められた 各祖先 Category オブジェクトには、NestedSet オブジェクトが関連付けられていない。これは上例のサンプルが、EntityManager のメソッド実行から連なる Categoryインスタンス生成を joinpoint としている一方、祖先 Cateogry の生成は Query.getResultList() 呼び出しから始まるものでコールスタックに EntityManager のメソッドが無いため。 これは EntityCreationWormhole アスペクトの callee ポイントカットに Query のメソッド実行を書き足せば、何とかなりそう。
  • Category のクラス定義で、抽象アスペクトを具象化しているが、できれば エンティティ・クラスは Java にしたい。本当は、こんな風に
    public interface NestedSet<T> {
       static aspect UseNestedSet extends NestedSetIdiom{}
       List<T> ancestors();
    }
    書きたかったが、文法的にだめらしい。
  • Adjacency List Model(自テーブルを再帰参照するやつ)との共存。
以下は、難しいのがわかっている事柄。
  • 追加・削除。

2009年12月21日月曜日

AspectJ で using 的な構文を模倣してみる (3)

前に AspectJ で using ぽい構文を試してみた。任意のクラスの解放メソッドを自動的に実行するところまでやったが、今回はネストした using ブロックを試してみる。 以下のようなコードで、、、
public class Client {
   public static void main(final String[] args) throws Exception {
      new UsingA() { A r1 = new A("r1"); {
         new UsingB() { B r2 = new B("r2"); {
            new UsingA() { A r3 = new A("r3"); {
               r1.use("x");
               r2.use("y");
               r3.use("z");
            }};
         }};
      }};
   }
}
、、、自動的に r3, r2, r1 の順にリソースが解放されるようにしてみたい。(前回までのコードでは r1 が解放されず r3 が2度解放されてしまっていた。) 以下のように Stack を使って UsingAspect を修正。ついでにマルチスレッドで使えるように percflow(using()) をアスペクト定義に付与。
public abstract aspect UsingAspect<T, U> percflow(using()) {
   protected Stack<T> stack = new Stack<T>();
   pointcut settingField(T o): set (T+ U+.*) && args(o);
   pointcut using(): call(U+.new(..));

   after(T o): settingField(o) {
      this.stack.push(o);
   }
   after(): using() {
      if (!stack.empty()) endUsing(this.stack.pop());
   }
   protected abstract void endUsing(T o);
}
その他のクラスは、ほとんど自明だけど以下のようなものになる。
UsingA
public interface UsingA {
   public static aspect AspectA
         extends UsingAspect<A, UsingA> {
      @Override
      public void endUsing(A o) {
         if (null != o) o.dispose();
      }
   }
}
UsingB
public interface UsingB {
   public static aspect AspectB
         extends UsingAspect<B, UsingB> {
      @Override
      public void endUsing(B o) {
         if (null != o) o.close();
      }
   }
}
A
public class A {
   private final String name;
   public A(String name) {
      this.name = name;
   }
   public void dispose() {
      System.out.printf("%s disposed%n", this);
   }
   public void use(String str) {
      System.out.printf("%s used with \"%s\"%n", this, str);
   }
   public String toString() {
      return String.format("A(\"%s\")", this.name);
   }
}
B
public class B {
   private final String name;
   public B(String name) {
      this.name = name;
   }
   public void close() {
      System.out.printf("%s closed%n", this);
   }
   public void use(String str) {
      System.out.printf("%s used with \"%s\"%n", this, str);
   }
   public String toString() {
      return String.format("B(\"%s\")", this.name);
   }
}
実行すると以下のようなコンソール出力を得る。
A("r1") used with "x"
B("r2") used with "y"
A("r3") used with "z"
A("r3") disposed
B("r2") closed
A("r1") disposed
割と簡単に using のネストができた。一応、だんだんと実用的になってきた気がする。取りあえずテストコードで使い始めて、パフォーマンスなんかを中心にしばらく観察してから、問題が無ければプロダクトコードで使ってみるような流れで良いか。

2009年12月18日金曜日

AspectJ で using 的な構文を模倣してみる (2)

前回のポストで試した using ぽい書法を、改善してみる。 前に書いた叩き台では、リソースクラスに解放用のインターフェイス Disposable を実装させていたが、任意のクラスに対応できるように抽象化する。こんなコードにしてみた。
public abstract aspect UsingAspect<T, U> {
  protected T obj;

  pointcut settingField(T o): set (T+ U+.*) && args(o);
  pointcut using(): call(U+.new(..));

  after(T o): settingField(o) {
    this.obj = o;
  }
  after(): using() {
    endUsing(this.obj);
  }

  protected abstract void endUsing(T o);
}
この Generic な抽象アスペクトを、それぞれのリソース型について以下のように具体的する。
  • 前回の Disposable オブジェクトの例。
    public interface UsingDisposable {
      public static aspect DisposableAspect
          extends UsingAspect<Disposable, UsingDisposable> {
        @Override
        public void endUsing(Disposable o) {
          if (null != o)((Disposable)o).dispose();
        }
      }
    }
  • java.io の Writer に適用する例
    interface UsingWriter {
      public static aspect UsingWriterAspect 
          extends UsingAspect<Writer, UsingWriter> {
        @Override
        protected void endUsing(Writer o) {
          try { if (null != o) o.close(); } 
          catch (IOException ex) { throw new RuntimeException(ex); }
        }
      }
    }
リソースオブジェクトごとに違うのは、つまるところオブジェクトの型と解放方法なので、カスタマイズするのはその部分だけに留めたい。取り敢えず一つの実装方法としてはこんな風なコードになる。( pointcut の定義で
pointcut using(): call(Using<T>+.new(..));
みたいなパラメタライズした書き方ができれば、更に堅牢で経済的なコードになるけど、残念ながら文法的に許されないらしい。) クライアントコードは以下のようになる
public class Client {
  public static void main(final String[] args) throws Exception {
    new UsingWriter(){
        PrintWriter s = new PrintWriter("test.txt"); {
      s.println("one line written");
      s.flush();
    }};
  }
}
デバッガを使うとか、コンソール入力を待つ行を追加するとかして、適当な箇所で止めながら動かすと、無名クラス生成ブロックの中でだけ ファイル test.txt にロックがかかっているのが確かめられる。 あとはマルチスレッドとネストか。 } } 実行すると以下のようなコンソール出力を得る。
A("r1") used with "x"
B("r2") used with "y"
A("r3") used with "z"
A("r3") disposed
B("r2") closed
A("r1") disposed
割と簡単に using のネストができた。一応、だんだんと実用的になってきた気がする。取りあえずテストコードで使い始めて、パフォーマンスなんかを中心にしばらく観察してから、問題が無ければプロダクトコードで使ってみるような流れで良いか。

2009年12月17日木曜日

AspectJ で using 的な構文を模倣してみる

C# の using のような感じで Java でも書いてみたいが、現時点の Java (SE 6) では文法的に無理があるので、AspectJ も併用して、どんな風になるか試してみる。 ■ 概要 要点は、あるコードブロックの冒頭でリソースの使用を宣言すると、そのコードブロックの終わりでリソースが自動的に開放されるという事。さらに、次の2点も実現したい。
  • そのスコープをブロックの内部に限定してリソースを隔離する事
  • try/finally のような感じで、例外発生時にもちゃんとリソースが開放される事。
■ 問題 以下のような模擬リソースクラスがあるとする。
public interface Disposable {
   void dispose();
}

public class SomeResource implements Disposable {
   public void write(String str) {
      System.out.println(str);
   }
   public void throwException() {
      throw new RuntimeException("something wrong");
   }
   @Override
   public void dispose() {
      System.out.println("disposed");
   }
}
この模擬リソースを[概要]の項に書いたような要件のもとで使う場合、普通の Java で書くと例えば以下のようなコードになる。
public class Client {
   public static void main(final String[] args) {
      {
         SomeResource d = new SomeResource();
         try {
            d.write(args[0]);
         } finally {
            if (null != d) d.dispose();
         }
      }
   }
}
これをリソースの扱いを簡略化した以下のようなコードに書き直して、なおかつ等価な動作を実現するような AspectJ コードを考えてみたい。d.write() の代わりに d.throwException()を呼んで例外を発生させても d.dispose() が実行される事にも留意する。
public interface Using {}
public class Client {
   public static void main(final String[] args) {
      new Using() { SomeResource d = new SomeResource(); {
         d.write(args[0]);
      }};
   }
}
■ 解1 一番単純な解の一つは、とりあえずこんな感じになると思う。
public aspect DisposeAspect {
   Disposable disposable;
   pointcut settingField(Object o): set (* Using+.*) && args(o);
   after(Object o): settingField(o) {
      disposable = (Disposable)o;
   }
   after(): call(Using+.new()) {
      disposable.dispose();
   }
}
実行引数として"hello"を与えて実行すると、コンソールに"hello"と"disposed"が表示される。また write() の代わりに throwException() を呼ぶとスタックトレースと"disposed"が表示される。 ■ 課題 上記コードは Proof of concept として書いた叩き台で、実用的なものにするにはさらに以下のような課題がある。
  • 抽象化して使いまわせるようにする事。例えば、Disposable 実装クラスだけじゃなくJDBC の Connection などのような任意のオブジェクトにも使えるようにしたい。
  • マルチスレッドで使えるようにする事。
  • ネストした場合について考えること
おいおい考えてみたい。

2009年12月16日水曜日

AspectJ : 派生した場合の優先順位

抽象アスペクトを extends して派生アスペクトを定義し、両方で同じ pointcut に advice したらどうなるか。以下のようなコードで試してみた。
package p3;

public class App {
   public static void main(String[] args) {
      new Foo().foo();
   }
}
class Foo {
   void foo() { System.out.println("foo"); }
}
abstract aspect AspectX {
   abstract pointcut p();
   before(): p() {System.out.println("before x.p");}
   after(): p() {System.out.println("after x.p");}
}
aspect AspectY extends AspectX {
   pointcut p() : execution(void p3.Foo.foo());
   before(): p() {System.out.println("before y.p");}
   after(): p() {System.out.println("after y.p");}
}
こんな出力になった(色分けは分かりやすくするため)。規定クラスが内側になるらしい。
before y.p
before x.p
foo
after x.p
after y.p

2009年12月14日月曜日

AspectJ: declare precedence

AspectJ の declare precedence の挙動を確認してみた。 以下のようなクラス App とアスペクト AspectX があるとする。
public class App {
  void foo() {
    System.out.println("hello");
  }
}
aspect AspectX {
  pointcut x(): execution(* *.foo(..));
  before() : x() {
    System.out.println("before x");
  }
  Object around(): x() {
    System.out.println("around(1) x");
    Object result = proceed();
    System.out.println("around(2) x");
    return result;
  }
  after() : x() {
    System.out.println("after x");
  }
}
これを実行すると、以下のような出力になる。
before x
around(1) x
hello
around(2) x
after x
ここで更に、AspectX とほぼ等価な内容の AspectY を追加したらどうなるか。
aspect AspectY {
  pointcut y(): execution(* *.foo(..));
  before() : y() {
    System.out.println("before y");
  }
  Object around(): y() {
    System.out.println("around(1) y");
    Object result = proceed();
    System.out.println("around(2) y");
    return result;
  }
  after() : y() {
    System.out.println("after y");
  }
}
出力は以下のようになる。分かりやすいように AspectX の出力を赤、AspectY の出力を青に色分けした。
before x
around(1) x
before y
around(1) y
hello
around(2) y
after y
around(2) x
after x
上の例では、たまたまAspectX が優先となっているが、この優先順位を明示するには、declare precedenceを記述する。(ここでは AspectY に追加した。)
aspect AspectY {
  declare precedence: AspectY, AspectX;
  pointcut y(): execution(* x.foo(..));
  before() : y() {
  ・・・略
こうすると、以下のように前後関係が逆転する。
before y
around(1) y
before x
around(1) x
hello
around(2) x
after x
around(2) y
after y

2009年12月13日日曜日

AspectJ design pattern: Participant

Aspectj in Action: 第一版に載ってた、Participant パターンについて。
class ClassA {
   public void foo() {
      System.out.println("ClassA.foo() called");
   }
   public void bar() {
      System.out.println("ClassA.bar() called");
   }
}
public class App {
   public static void main(String[] args) {
      ClassA a = new ClassA();
      a.foo();
      a.bar();
   }
}
上のコードに作用して、以下のように星印をつけてコンソール出力を修飾する Star アスペクトがあるとする。
アスペクト適用なしStar アスペクト適用
ClassA.foo() called
ClassA.bar() called
 ⇒ 
★ClassA.foo() called
ClassA.bar() called
上の例では★がついているのは ClassA.foo() だが、後々の拡張性を考えて、どのメソッドが★印修飾されるかについて、アスペクト定義の側にはハードコードしたくない。また、後で★印修飾を適用するメソッドは、ジョインポイントにマッチさせるためのシグネーチャの制約(例えば命名規約だとか)をつけたくない。こういう状況で、Participant パターンが適用できる。 まず以下のように抽象アスペクトを定義する
public abstract aspect Star {
   public abstract pointcut needStar();
   Object around() : needStar() {
      System.out.print("★");
      return proceed();
   }
}
次に、ClassA のクラス定義内で具象アスペクトを定義する。
class ClassA {
   public static aspect StarForA extends Star {
      public pointcut needStar(): call(void ClassA.foo());
   }
   public void foo() {
   ...略
}
こんな風にして、上図右のような★印修飾がついたコンソール出力が得られる。 さて、後でClassB を追加して、App.main() を以下のように変更したとする
public static void main(String[] args) {
   ClassA a = new ClassA();
   ClassB b = new ClassB();
  
   a.foo();
   a.bar();
   b.baz();
}
ここで、メソッド baz() にも★印修飾を与えたい場合、以下のように書けばよい。
class ClassB {
   public static aspect StarForB extends Star {
      public pointcut needStar(): call(void ClassB.baz());
   }
   public void baz() {
      System.out.println("ClassB.baz() called");
   }
}
こうすれば、Starアスペクトへの変更も、baz() へのシグネーチャ制約も無く、以下のような出力が得られる。
★ClassA.foo() called
ClassA.bar() called
★ClassB.baz() called
■所感
  • GoF の Template パターンでは、変更を与えるポイントが決まっていて、そのポイントでの振る舞いをカスタマイズする。一方、この Participant パターンでは、ある一定の振る舞いの変化を、どこに適用するかについてカスタマイズする。つまり Template パターンとParticipant パターンで再利用部分とカスタマイズ部分が逆になっているが、これが OOP と AOP の典型的な違いのような気がする。
  • 具象側のアスペクトでポイントカットを定義する代わりに、アノテーション を使うやり方もある。上の例では、内部アスペクトを定義してクラス単位のジョインポイント管理にしていたが、アノテーションを使うとメソッド単位で アスペクトに参加するメソッドを宣言することになり、より粒度の細かい管理になる。
    pointcut p() : execution(@Hoge * *.*());

2009年12月12日土曜日

AspectJ: Worker Object Creation pattern

AspectJ デザインパターンの Worker Object Creation というのをやってみる。 下のコードを実行すると、コンソールに1,2,3 と順番に表示されるが、これを3,2,1 と逆順で実行されるようにしてみる。本番の開発では、そんなことしないだろうけど、実験として試してみたい。
public class App {
  void foo() {
    bar(1);
    bar(2);
    bar(3);
  }
  void bar(int number) {
    System.out.println(number);
  }
  public static void main(String[] args) {
    new App().foo();
  }
}
AspectJ In Action (古い版)に Worker Object Creation という技が載っていて、メソッド呼び出しをオブジェクトにくるんで管理したり間接的に実行するパターンが、そう呼ばれている。これを応用すると以下のようなアスペクト定義で、たとえば上のような逆に実行すといった制御も実現できる。
public aspect Reverse {
   static interface Worker { void run(); } 
   Stack<Worker> history = null;
   pointcut callFoo(): execution(void App.foo());
   pointcut callBar(): execution(void App.bar(int));
   before (): callFoo() {
      history = new Stack<Worker>();
   }
   after (): callFoo() {
      while (!history.empty()) history.pop().run();
   }
   void around(): callBar() {
      history.push(new Worker(){
         public void run() { proceed(); }
     });
   }
}
動かしてみると、bar() 呼び出しが逆順に実行され、コンソールに 3,2,1 と表示される。 AspectJ In Action のサンプルコードでは、メソッド呼び出しを Runnable.run()からproceed() して非同期にしている。また同書10章「Authentication and Authorization」の、PrivilegedExceptionAction.run() からproceed() しているイディオムも、worker object creation の一例。おそらくこういった別スレッド化やセキュリティ対応などといったアスペクトが、このパターンの典型的な使い型だろうけど、やればこんな変な事もできるというのを試してみた。

2009年12月11日金曜日

AspectJ design pattern: Wormhole

Aspectj in Action: 第一版に載ってた、デザインパターン。
public class ClassA {
   ClassB b = new ClassB();
   void foo(int count) {
      for (int i = 0; i < count; ++i) b.bar(i);
   }
}
class ClassB {
   void bar(int i) { new ClassC().baz(i); }
}
class ClassC {
   void baz(int i) { System.out.println(i); }
}
上のコードを "new ClassA().foo(3);" のように実行すると、下図左のような出力になるが、これを下図右のように最後の1行だけ修飾したい。
0
1
2
0
1
last -> 2
このためには baz() 呼び出し時に、foo() 呼び出し時に渡された count が参照できなければならないが、bar()、baz() のシグネーチャは汚したくない。こういうときに AspectJ の Wormhole パターンが使える。 もとのコードは変えないまま、こんなアスペクトで上記の結果が得られる。
public aspect AspectX {
   pointcut foo(int count) : execution(void ClassA.foo(int)) && args(count);
   pointcut baz(int i) : execution(void ClassC.baz(int)) && args(i);
   pointcut wormhole(int count, int i): cflow(foo(count)) && baz(i);
   before(int count, int i) : wormhole(count, i) {
      if (count == i + 1) System.out.print("last -> ");
   }
}

2009年11月5日木曜日

JAAS/AJDT/Eclipse

Eclipse 3.5(Galileo) に AspectJ 2.0.1 を入れたので、AspectJ による認証コードの分離を Eclipse上でやってみる。 以下のような流れで。
  1. 素のコンポーネントを作って、よそから自由に使える事を確認する。
  2. このコンポーンネントに権限チェックを追加して、勝手に使えなくする。
  3. 認証された権限保持者が正しく実行すると、コンポーネントが使えるのを確認。
  4. これらのプロジェクトを AspectJで リファクタする
■ 1 素のコンポーネントを作る
  • confidential-component という名前の Java Project を作る。
  • 以下のクラスを作る。
    package study.jaas_aop.component;
    public class ConfidentialComponent {
       public String summary() {
          return "summary";
       }
       public String detail() {
          return "detail";
       }
    }
  • client という名前の Java Projectを作る。
  • Java Build Path の Projectタブ で、confidential-componentプロジェクトへの参照を指定する。
  • もう一つ、以下のクラスを作る。
    package study.jaas_aop.client;
    
    import study.jaas_aop.component.ConfidentialComponent;
    
    public class App {
       public static void main(String[] args) {
          ConfidentialComponent comp = new ConfidentialComponent();
          System.out.println(comp.summary());
          System.out.println(comp.detail());
       }
    }
  • 実行して、"summary" "detail" と2行表示されるのを確認する。
まあ、ここまで当たり前。 ■ 2 パーミッションチェックを行う
  • confidential-component プロジェクトで、以下のようなパーミッションクラス MyPermission を作る
    package study.jaas_aop.component;
    import java.security.BasicPermission;
    
    public class MyPermission extends BasicPermission {
       public MyPermission(String name) {
          super(name);
       }
    }
  • ConfidentialComponent の summary()、detail() で checkPermission()する
    public String summary() {
       AccessController.checkPermission(new MyPermission("summary"));
       return "summary";
    }
    public String detail() {
       AccessController.checkPermission(new MyPermission("detail"));
       return "detail";
    }
  • 実行して、"summary"でアクセス例外が出るのを確認する
    Exception in thread "main" java.security.AccessControlException: access denied (study.jaas_aop.component.MyPermission summary)
勝手には使えないようになった。 ■ 3. 正当なクライアントが、正しい作法で呼び出す まずは呼び出し側の変更から解説して、そのためのログインモジュール(ダミー)を作ってみる。 ◆呼び出し側の変更
  1. summary(), detail() 両メソッドの呼び出しを PrivilegedAction でラップして、Subject.doAsPrivileged()経由で実行
  2. Subject は、LoginContext から得る
  3. Subject を返す前に、LoginContext の login() メソッドが実行されている必要がある。
まとめると以下のコードになる。
public static void main(String[] args) throws Exception {
   LoginContext lc = new LoginContext("study");
   lc.login();
   Subject subject = lc.getSubject();

   final ConfidentialComponent comp = new ConfidentialComponent();
   Subject.doAsPrivileged(subject, new PrivilegedAction<Object>() {
      @Override
      public Object run() {
         System.out.println(comp.summary());
         return null;
      }
   }, null);
   Subject.doAsPrivileged(subject, new PrivilegedAction<Object>() {
      @Override
      public Object run() {
         System.out.println(comp.detail());
         return null;
      }
   }, null);
}
LoginContext のコンストラクタに渡している文字列"study"は、ログイン設定ファイル内のエントリ名で、これをキーとしてログインモジュール(後述)が関連付けられる。ここではファイル名 my_login.config とし、client プロジェクトディレクトリ直下に置く。
study {
  study.jaas_aop.lm.DummyLoginModule required;
};
このログイン設定ファイが実行時に読み込まれるように、[Run Configuration]の[VM arguments]で以下のように指定する。
-Djava.security.auth.login.config=my_login.config
◆ログインモジュール作成 続いて上記ファイルで指定した、DummyLoginModule を作る。ただし、今回はクライアント目線中心でやってるので、本格的な認証ロジックは省略したダミーのログインモジュールとする。つまり LoginContext.login()が呼ばれると、常に固定 ユーザ"manager"がログイン成功するという筋書きとする。(後で"manager"を"worker"に変える。) プロジェクト名 login-module で Java Project を作成し、以下の2クラスを書く
package study.jaas_aop.lm;

import java.util.Map;

import javax.security.auth.Subject;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

public class DummyLoginModule implements LoginModule {
   private static final String USER_NAME = "manager";
   private Subject subject = null;
   public void initialize(Subject subject, 
         CallbackHandler callbackHandler,
         Map<String, ?> sharedState, 
         Map<String, ?> options) {
      this.subject = subject;
   }
   public boolean login() {
      return true;
   }
   public boolean commit() throws LoginException {
      subject.getPrincipals().add(new DummyPrincipal(USER_NAME));
      return true;
   }
   public boolean abort() throws LoginException {
      return false;
   }
   public boolean logout() throws LoginException {
      return false;
   }
}
package study.jaas_aop.lm;

import java.security.Principal;

public class DummyPrincipal implements Principal {
   private String name;

   public DummyPrincipal(String name) {
      this.name = name;
   }
   public String getName() {
      return name;
   }
}
上記コードを簡単に説明すると、App.main() での LoginContext.login() 実行中に、DummyLoginModule のinitialize(), login(), commit()が順に呼ばれ、最終的に ユーザ"manager"に対応づく Principal が、Subject に関連付けられるという事になる。(注意:あくまでもダミーなので、ちゃんと本物の LoginModule を書くときの参考にはならない。) さて、これでログインモジュールができたが、"manager"が認証されたとして、何をする事が許されているのかを、さらに定義する必要がある。これは プロジェクトディレクトリ直下に security.policy ファイルを置いて、そこで ConfidentialComponent のメソッドへのアクセス権を指定する事にする。
grant Principal study.jaas_aop.lm.DummyPrincipal "manager" {
  permission study.jaas_aop.component.MyPermission "summary";
  permission study.jaas_aop.component.MyPermission "detail";
};
このsecurity.policyが実行時に読まれるようにするためには、VM argument で以下の指定を追加する。
-Djava.security.policy=security.policy
あと、[Configure Build Path] で、client プロジェクトから login-moduleプロジェクトへの参照も設定しておく。 以上で、準備ができたので動かしてみる。ちゃんと"summary"と"detail"の2行が表示されているはず。 ここで manager の他に、もう一人 worker というユーザもいる状況を試してみる。この人には summary()へのアクセスは許可されているが、detail()へのアクセスは許されていない。 security.policy中の manager プリンシパルの下に、以下を追記する。
grant Principal study.jaas_aop.lm.DummyPrincipal "worker" {
 permission study.jaas_aop.component.MyPermission "summary";
};
DummyLoginModule の USER_NAME を"worker"に変更して、再度試してみると、"summary"と表示された行の下に、AccessControlException がスタックトレースされ、"access denied (~~detail)"と言うメッセージが出ているはず。 とまあこんな感じでsecurity.policy ファイルの設定が効いているのが分かる。以後、security.policy の効き目がはっきりわかるように、USER_NAME は"worker"のままにしておく。 ■ 4. AspectJでリファクタする 認証コードを追加した結果、一番最初の App.main()と比べると、関心の異なるコードが入り乱れてかなりゴチャゴチャしている。これをまず直してみる。
  • client プロジェクトのコンテキストメニューから、[Convert to AspectJ Project]を実行する
  • 新規アスペクト"AuthAspect"を作成する。
    package study.jaas_aop.client;
    
    import java.security.PrivilegedAction;
    
    import javax.security.auth.Subject;
    import javax.security.auth.login.LoginContext;
    import javax.security.auth.login.LoginException;
    
    import study.jaas_aop.client.App;
    import study.jaas_aop.component.ConfidentialComponent;
    
    public aspect AuthAspect {
      private Subject subject;
    
      public pointcut upfontLoginPoint()
         :execution(void App.main(String[]));
    
      public pointcut authOperationCalls()
        :call(public * ConfidentialComponent.*(..));
    
      before() : upfontLoginPoint() {
        try {
          LoginContext lc = new LoginContext("study");
         lc.login();
         subject = lc.getSubject();
        } catch (LoginException ex) {
          throw new RuntimeException(ex);
        }
      } 
      Object around() 
        : authOperationCalls() && !cflowbelow(authOperationCalls()) {
          return Subject.doAsPrivileged(subject, 
            new PrivilegedAction<Object>() {
              public Object run() {  return proceed();  }
            }, null);
      }
    }
  • App.main()を、元の認証コードのないやつに戻す。
  • ユーザ"worker"で実行してみると、summary() 成功/detail()失敗となり、ちゃんと認証が利いているのがわかる。
ついでに confidential-component 側も AOP でリファクタしておく。
  • confidential-component プロジェクト上で[Convert to AspectJ Project]を実行
  • 新規アスペクト PermAspect を作成
    package study.jaas_aop.component;
    
    import java.security.AccessController;
    
    public aspect PermAspect {
      public pointcut authOperations()
        :execution(public * ConfidentialComponent.*(..));
     
      before() : authOperations() {
        String name = thisJoinPointStaticPart.getSignature().getName();
        AccessController.checkPermission(new MyPermission(name));
      }
    }
  • ConfidentialComponent の checkPermission() を呼んでいる行を削除して、一番最初のやつにもどす。
  • 実行して、挙動に変化が無く正しく動いている事を確認する。
以上、認証コード記述から AOP によるリファクタまで。 パッっと見、アスペクトのコードが多めに見えるかもしれないけど、コンポーネントの呼び出し箇所が増えたり、コンポーネント自体が複雑なロジックを持つようになったら、分離による効果が高くなるのはすぐ分かると思う。

AJDT: weaving service の restart 問題

Aspect Jプラグイン(AJDT)をインストールした。

問題が二つ発生したので、解決策を残しておく。
■ 環境
  • Eclipse 3.5
  • AspectJ Development Tools 2.0.1

■ JDT のコンフリクトでインストール失敗
  • update サイト「http://download.eclipse.org/tools/ajdt/35/update」から [Install New Software]でインストールしようとすると、JDT 関連のコンポーネントがコンフリクトしてエラー発生。一度に一バージョンしかインストールできない、排他的な組み合わせがあるらしい。
  • Groovy プラグインと Scala プラグインが依存している JDT コンポーネントが、AspectJ が必要とする JDT コンポーネントとの干渉が解決できないらしい。
  • とりあえず Groovy と Scala をアンインストールすると、無事 AspectJがインストールできた。その後で Scala は復帰できたが、Groovy ダメだった。とりあえず併用する予定はないので、深入りは止めた。

■ 何度も restart を要求される。
  • Eclipse を起動するたびに「Should AJDT's weaving service be enabled」で restartしろといってくる。それで再起動しても、また同じ事を言ってくる。
  • {eclipse}/configuration/org.eclipse.equinox.simpleconfigurator/bundles.infoを開いて、org.eclipse.equinox.weaving.aspectjではじまる行の末尾の false を trueに変更する。保存して再起動すると問題解消。
  • 参考URL:http://www.myeclipseide.com/PNphpBB2-printview-t-23405-start-0.html