BT

最新技術を追い求めるデベロッパのための情報コミュニティ

寄稿

Topics

地域を選ぶ

InfoQ ホームページ アーティクル Spring2.0とAspectJでエンタープライズアプリケーションを単純化する

Spring2.0とAspectJでエンタープライズアプリケーションを単純化する

Springはエンタープライズアプリケーションの開発を可能な限りシンプルかつ生産的にすることを目指しています。この理念の具体例は、JDBC、 ORM、JMX、DIやその他多くのエンタープライズアプリケーション開発における重要な領域に対するSpringのアプローチに見ることができます。ただしSpringは効果的な単純化と過度の単純化をしっかり区別します。シンプルさとパワーは同時には得難いものです。エンタープライズアプリケーションにおける複雑さのひとつは、アプリケーションの複数箇所に影響を及ぼす機能や要件を実装するときに生まれます。そういう関連のコードはアプリケーションのあちこちに撒き散らされる結果となり、機能追加やメンテナンスや理解を困難にします。Spring2.0はそういった機能の実装をモジュール開発のマナーに則って単純化し、アプリケーションコード全体をシンプルに、そしてSpring2.0を使っていなければコーディングが苦痛だっだと思われる要件を実装し易くしてくれます。

トランザクション管理はアプリケーションのあちこちに影響を及ぼす機能の一例です。一般的にはサービスレイヤの全操作がその影響を受けます。このような要件をSpringではAOPを使って取り扱います。Spring2.0のAOPサポートは、Spring1.xを超える豊かな表現力を提供すると同時に、大幅な単純化が行われています。機能は二つの主要な領域で向上しました。XMLスキーマを用いることで設定を大幅に単純化したのと、AspectJとの統合によって一層豊かな表現力とシンプルなアドバイスモデルを実現したことです。

この記事で私は、一般的なエンタープライズアプリケーションのどういう場所にSpring AOPとAspectJを利用するのが適しているのかを最初に述べ、続いてSpring2.0における新しいAOPサポートをお見せします。記事の大部分では、他の方法では困難でもAOPを使えばシンプルな実装が可能な機能の例をたくさん使って、エンタープライズアプリケーションにおけるAOPの導入手順を中心に見ていきます。

エンタープライズアプリケーションをシンプルに

一般的なエンタープライズアプリケーション(WEBアプリのことです)は、複数のレイヤから構成されています。ビューとコントローラから成るWEBレイヤ、システムのビジネスインターフェースを表わすサービスレイヤ、永続的なドメインオブジェクトの保存や検索を担当するデータアクセスレイヤまたはリポジトリレイヤ、そしてこれら全てと並行して利用される、コアとなるビジネスロジックをもつドメインモデルです。

WEBレイヤ、サービスレイヤ、データアクセスレイヤはいくつかの重要な特性を共有しています。それらはできる限り薄くあるべきで、ビジネスロジックを含むべきではありません。一般的にはSpringを使って互いに紐付けを行います。これらのレイヤにおいてSpringはオブジェクトの生成と設定を担当します。ドメインモデルはやや異なり、ドメインオブジェクトはプログラマがnewオペレータを使用して生成します(あるいはORMツールを使ってデータベースから検索して取得します)。ドメインオブジェクトには多くの一意なインスタンスがあり、それらはリッチな振る舞いを持つことができます。

サービスレイヤがアプリケーションのユースケースで定義されたロジックを含むのは問題ありませんが、ドメインに関連する全てのロジックは、ドメインモデル自身に含めるべきです。

一般にサービスレイヤは宣言的なエンタープライズサービス(トランザクションなど)が用いられる場所です。トランザクションやセキュリティのような宣言的なエンタープライズサービスはアプリケーションのあちこちで必要とされる要件のよい例です。それに、仮にトランザクション境界が一箇所にしか必要ないとしても、その機能をアプリケーションロジックから切り離すことは、コードをシンプルに保ち、不必要な結合を避けるという意味で価値があります。

サービスオブジェクトはSpringによって管理されているbeanなので、そのレイヤ内で要件を取り扱う上でSpring AOPは適任です。実は、Springの宣言的トランザクションを使っている人はすでにSpring AOPを使用しているのです。そのことを自覚しているかどうかは別として。Spring AOPは成熟した機能で、幅広く用いられています。WEB、サービス、データアクセス各レイヤ内にあるSpring管理のbeanで、そのメソッドを実行することで要件を処理できるものには、AOPの使用がよくフィットします(そして、これらのレイヤの多くのユースケースがこれに合致します)。

ドメインモデルはアプリケーションの最も重要な部分ですが、そのドメインモデルの複数個所に影響を及ぼす要件については、Spring AOPはそれほどの支援をしません。Spring AOPをプログラムで用いることもできないわけではありませんが、それだと非常に不恰好ですし、プロキシの生成や同一性の管理を全て自分で行わなくてはなりません。AspectJはドメインオブジェクトに影響を及ぼす機能の実装に適しています。AspectJのアスペクトは特別なプロキシの生成が一切不要で、自分のアプリケーションコード内でもしくは使用しているフレームワークによって実行時に生成されたオブジェクトに対してうまくアドバイスを適用できます。アプリケーション内の異なる全てのレイヤにまたがる振る舞いや、パフォーマンスに影響を与えやすい振る舞いのモジュール化が求められる場合にも、 AspectJは非常によい解決方法です。

だから、私たちが求める理想は、Spring AOPとAspectJの二つを簡単に同時に扱うことができ、また要件が大きくなったときに(例えば)Spring AOPを使った開発スキルをAspectJを使ったものに転用可能であるような、両者への一貫したアプローチです。私たちが使っているどのような連携においても、Springが提供するDIとコンフィギュレーションの利益はやはり求められます。Spring2.0における新しいAOPサポートは正にこれを提供してくれるのです。

基礎技術:AspectJとSpring AOPの概要

AOPはアプリケーションの複数個所に影響を及ぼすような機能の実装をとても簡単にしてくれます。その主な理由は、AOPがアドバイスとして知られるものをサポートしてくれるからです。明示的に呼び出さなければならないメソッドとは違って、アドバイスはそれにマッチするトリガーイベントが発生したときに自動的に実行されます。トランザクションをテーマにして話を続けると、トリガーイベントはサービスレイヤのメソッドの実行で、アドバイスのロジックは必要とされるトランザクション境界を提供します。AOPの用語では、このトリガーイベントのことはジョインポイントとして知られ、アドバイスが実行されるジョインポイントを選択するためにポイントカット式が用いられます。このシンプルな発想の逆転は、アプリケーションコード全体にわたってトランザクションマネージャの呼び出しをまき散らかさなければならない代わりに、単にポイントカット式を書いてトランザクションマネージャを呼び出したい全ポイントを指定し、それに対応するアドバイスを関連付ければよいということです。AspectJとSpring AOPは共にこのモデルをサポートしており、実はまったく同じポイントカット式言語を共有しているのです。

続く議論では、SpringとAspectJが依然として別々のプロジェクトであるということを心に留めておくことが大事です。Springは、AspectJによってライブラリとして提供されているリフレクションとtools APIを単に使用しています。Spring2.0は依然として実行時のプロキシベースのフレームワークであり、AspectJのウィーバーはSpringのアスペクトには使用されていません。

皆さんの大部分がご存知のとおり、AspectJは完全なコンパイラ(Eclipse JDT Javaコンパイラの拡張として構築されている)をもった言語で、コンパイル時またはVM内にクラスがロードされる時点における、クラスファイルへの(関連するアスペクトの)ウィービングをサポートしています。AspectJの最新リリースはAspectJ5で、Java5を完全にサポートしています。

AspectJ5はアスペクト定義の第二のスタイルも導入しました。私たちはそれを「@AspectJ」と呼んでいます。@AspectJでは、アノテーションを用いることで正規のJavaクラスとしてアスペクトを記述することができます。アスペクトは正規のJava5コンパイラでコンパイル可能です。たとえば、AspectJプログラミング言語のお決まりの「HelloWorld」アスペクトは次のようになります。

public aspect HelloFromAspectJ {
  pointcut mainMethod() : execution(* main(..));

 after() returning : mainMethod() {
 System.out.println("Hello from AspectJ!);
}
}

このアスペクトをお決まりHelloWorldクラスと同時にコンパイルして実行すると次のように出力されます。

Hello World!
Hello from AspectJ!

同じアスペクトを@Aspectスタイルを使って次のように書くことができます。

@Aspect
public class HelloFromAspectJ {

  @Pointcut("execution(* main(..))")
 public void mainMethod() {}
 @AfterReturning("mainMethod()")
 public void sayHello() {
 System.out.println("Hello from AspectJ!");
}
}

この記事の目的にとって重要なAspectJ5の他の新機能は、AspectJを十分に意識したリフレクションAPI(実行時にアスペクトのもつアドバイスやポイントカットなどを調べることが可能)と、サードパーティ製品によるAspectJのポイントカットの解析・マッチングエンジンの利用を可能にする tools APIです。これらのAPIを使う最初の重要なユーザが、間もなくわかるとおり、Spring AOPなのです。

AspectJとは異なり、Spring AOPはプロキシベースの実行時フレームワークです。Spring AOPは、使うのに特別なツールやビルドは必要ないので、AOPを始めるためのとても簡単な方法です。プロキシベースのフレームワークであることには、メリットとデメリットの両方があります。すでに述べた使いやすさに加えて、プロキシベースのフレームワークは同じ型の異なるインスタンスに独立にアドバイスを適用することができます。これはある型の全てのインスタンスが同じ振る舞いをもつAspectJのタイプベースのセマンティクスとは対照的です。 Springのようなフレームワークにとって、個別のオブジェクト(Springのbean)に別々にアドバイスを適用できるのは重要な要件なのです。マイナス面としては、Spring AOPはAspectJのもつ機能を部分的にしかサポートしていません。Spring AOPはSpring beanのメソッド実行(execution)に対してアドバイスを適用できますが、それ以外の場所に適用することはできません。

プロキシベースのフレームワークは、概してアイデンティティの問題に悩まされます。アプリケーションには、同じエンティティを表わしている二つのオブジェクト(プロキシとターゲット)があります。常に適切な参照を渡し、インスタンス化された全ての新しいターゲットオブジェクトについて確実にプロキシを生成することには慎重さが必要です。Spring AOPは、beanのインスタンス化を管理し(プロキシを透過的に生成可能)、DIを通す(Springが常に適切な参照をインジェクト可能)ことでこれらの問題を鮮やかに解決します。

Spring2.0における新しいAOPサポート

2.0におけるSpring AOPはSpring1.xのアプリケーションおよびコンフィギュレーションと完全な後方互換性を保っています。また、Spring1.xと比べて一層シンプルかつパワフルなコンフィギュレーションを提供しています。新たなAOPサポートはスキーマベースなので、関連のあるネームスペースとスキーマロケーション属性がSpring beanのコンフィギュレーションファイルに必要です。コンフィギュレーションファイルがどのようになるのか次に示します。


xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation=
"http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

...

DTDを使うときに必要とされる、より簡単なXMLコンフィギュレーションと比較すると、新しいコンフィギュレーションは今のところ勝っているとは言えません。ですがこれは標準的なXMLコンフィギュレーションですし、IDEのテンプレートに設定しておけばSpringのコンフィギュレーションを生成する必要があるときに簡単に再利用することができます。そしてコンフィギュレーションにいくつかの内容を追加し始めたとき、皆さんはそのメリットを知ることになるでしょう。

Spring2.0はデフォルトでAspectJのポイントカット言語を使用します(executionジョインポイントの種類に制限されます)。もしそれがAspectJのポイントカット式であれば、AspectJを呼び出して解析・マッチを行います。これはつまりSpring AOPで記述したポイントカット式は全てAspectJと全く同じ方法で動作するということです。さらに、Springは実は@AspectJのアスペクトを理解できます。従って、全てのアスペクト定義をSpringとAspectJの間で共有することができます。この機能を有効にするのは簡単で、 要素をコンフィギュレーションに含めるだけです。Aspectj-autoproxying がこの方法で有効になると、appliation contextファイルにおいて@AspectJアスペクト型で定義されている全てのbeanはSpring AOPによってアスペクトとして解釈され、それに応じてアドバイスが適用されます。

この方法でSpring AOPを使っているHello Worldプログラムを示します。まずapplication contextファイルのbeans要素の内容です。

	
class="org.aspectprogrammer.hello.spring.HelloService"/>




class="org.aspectprogrammer.hello.aspectj.HelloFromAspectJ"/>

HelloServiceは単なるJavaクラスです。

public class HelloService {

public void main() {
System.out.println("Hello World!");
}

}

HelloFromAspectJはこの記事の前のほうで見たのと全く同じJavaクラス(@AspectJアスペクト)です。次に小さなメインクラスを示します。このクラスはSpringのコンテナを立ち上げ、helloServiceへの参照を取得し、そのmainメソッドを呼び出します。

public class SpringBoot {

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext(
"org/aspectprogrammer/hello/spring/application-context.xml");
HelloService service = (HelloService) context.getBean("helloService");
service.main();
}

}

このプログラムを実行すると次のような出力が得られます。

Hello World!
Hello from AspectJ!

これは依然としてSpring AOPです(AspectJのコンパイラやウィーバーは一切使っていません)が、@AspectJアスペクトについてのリフレクション情報を AspectJが提供し、Springに代わってポイントカットの解析やマッチングを行っているということを覚えておいてください。

Spring2.0は、単なるPOJO(アノテーション一切不要)を使ったアスペクト定義のためのXML記述もサポートしています。XML記述も、他のアスペクト定義と同じAspectJのポイントカット言語の一部を使用し、同じ五つのアドバイスタイプ(before、after returning、after throwing、after [finally]、around)をサポートします。

XMLベースのアスペクト宣言を使ったHello Worldアプリケーションがどのようになるか、次に示します。

   
class="org.aspectprogrammer.hello.spring.HelloService"/>









class="org.aspectprogrammer.hello.spring.HelloAspect"/>

aopネームスペースの要素はアスペクト、ポイントカット、アドバイスをAspectJや@AspectJとまったく同じセマンティクスを使って宣言するために使うことができます。aspect要素はSpringのbean(完全にSpringによって設定、インスタンス化される)を参照し、各advice要素はアドバイスを実行するために呼び出されるbean上のメソッドを定義します。このケースでは、HelloAspectは次のように簡単に記述できます。

public class HelloAspect {

public void sayHello() {
System.out.println("Hello from Spring AOP!");
}

}

このプログラムを実行すると見慣れた出力が得られます。

Hello World!
Hello from Spring AOP!

これは、もしまだ試していないなら、Spring2.0をダウンロードしていくつかを自分で試してみるよいポイントだと思います。

この記事をSpring AOPの完全なチュートリアルにしてしまうよりも、私はこの方法を効果的に実装した機能の例をいくつか見ることを勧めたいと思います。ついでに指摘しておくと、AspectJのポイントカット言語を使うことでSpringが得るもののひとつは、いつも型付けされないオブジェクト配列で作業をしているのとは反対に、静的に型付けされたアドバイス(実際に必要とされるパラメータを宣言しているメソッド)を記述可能だということです。これはアドバイスメソッドの記述を一層シンプルにしてくれます。

導入手順

理屈はもう十分でしょう。どうやって、そして何のために、エンタープライズアプリケーションにAOPを実際に用いるかについて、いくつかの例を見てみましょう。AOPを始めるのに、ゼロか全てかのビッグバンアプローチを採る必要はありません。導入は段階的に進めることができます。各フェーズは、技術的に高度になってくる代わりにより多くの利益をもたらしてくれます。

お勧めの導入手順は、Springが提供する追加設定のいらない単純なアスペクト(例えばトランザクション管理)から始めることです。Springユーザの多くは、もしかするとAOPが隠蔽されていることの価値に気付かないままで、すでにこれを行っているかもしれません。そしてその後で、WEBレイヤ、サービスレイヤ、データアクセスレイヤに横断的な独自の要件をSpring AOPを使って実装すればよいのです。

ドメインモデルに影響を及ぼすような機能の実装にはAspectJの使用が必要です。運用中のアプリケーションには決して作用しない開発用の便利なたくさんのアスペクトがAspectJにあることに皆さんは驚くかもしれません。これらのアスペクトには大きな価値があり、導入リスクが小さいので、 AspectJを使用し始めるのにお勧めです。それに続いて構造の基礎をなす要件(典型的な例としてプロファイリング、トレーシング、エラーハンドリングなど)をAspectJを用いて実装することを選択するのもよいでしょう。AspectJと付随ツールの利用に慣れてきたら、最終的にドメインロジックの機能自体をアスペクトを使って実装し始めてもかまいません。

AOP導入の手順についての更なる情報については『Eclipse AspectJ』(source)の第11章か、Ron BodkinがDeveloper WorksのAOP@Workシリーズで執筆した『Next steps with aspects』(source)を見てください。これらはいずれも専らAspectJにフォーカスを当てていますが、SpringとAspectJをいっしょに用いているところも見ることができます。

これらの導入の各段階を順番に見ていきましょう。

プロジェクトでAOPを用いるときに最初にやると便利なのが、アプリケーションの異なるモジュールやレイヤを記述する一連のポイントカット式を定義することです。これらのポイントカット式は導入の全ての段階にわたって利用することができますし、定義を一度だけにすることで重複を減らしコードの可読性を向上させます。@AspectJでこれらのポイントカットを書けば、正規のJava5コンパイラでそれらをコンパイルできます。同じことを正規の AspectJ言語を使って記述して、ajcを使ってソースファイルをコンパイルし、作成された.classファイルをクラスパスに追加することも可能です。二つのうちSpring AOPを始めるにあたってより簡単である@AspectJを使ってみます。読者の多くはSpringに付随してくるjpetstoreサンプルアプリケーションをご存知でしょう。私はこのアプリケーションを軽くリファクタリングし、アスペクト(この記事の後のほうで議論します)をいくつか追加してみました。ペットストア内のメインレイヤとモジュールをキャプチャする「SystemArchitecture」アスペクトの最初のバージョンを次に示します。

@Aspect
public class SystemArchitecture {

/**
* we're in the pet store application if we're within any
* of the pet store packages
*/
@Pointcut("within(org.springframework.samples.jpetstore..*)")
public void inPetStore() {}

// modules
// ===========

@Pointcut("within(org.springframework.samples.jpetstore.dao..*)")
public void inDataAccessLayer() {}

@Pointcut("within(org.springframework.samples.jpetstore.domain.*)")
public void inDomainModel() {}

@Pointcut("within(org.springframework.samples.jpetstore.service..*)")
public void inServiceLayer() {}

@Pointcut("within(org.springframework.samples.jpetstore.web..*)")
public void inWebLayer() {}

@Pointcut("within(org.springframework.samples.jpetstore.remote..*)")
public void inRemotingLayer() {}

@Pointcut("within(org.springframework.samples.jpetstore.validation..*)")
public void inValidationModule() {}

// module operations
// ==================

@Pointcut("execution(* org.springframework.samples.jpetstore.dao.*.*(..))")
public void doaOperation() {}

@Pointcut("execution(* org.springframework.samples.jpetstore.service.*.*(..))")
public void businessService() {}

@Pointcut("execution(public * org.springframework.samples.jpetstore.validation.*.*(..))")
public void validation() {}

}

これでアプリケーションについて語るための語彙が手に入りました(「inServiceLayer」「businessService」など)。これを使って何か有益なことをしてみましょう。

追加設定不要なSpringのアスペクトを使う

アドバイザはSpring1.1から持ち越されたSpringの概念で、単一のアドバイスと関連するポイントカット式をもつごく小さなアスペクトを表します。トランザクション境界の定義では必要なのはひとつのアドバイザだけです。一般的なトランザクションの要件は、サービスレイヤの全ての操作を、背後にあるリソースマネージャのデフォルトの分離レベルのトランザクション(振る舞いはREQUIRED)で実行することです。さらに、いくつかの操作は「read-only」なトランザクションであるとしてマークしてもかまいません。その情報はトランザクションのパフォーマンスを大きく向上させるはずです。jpetstoreのアドバイザの定義は次のとおりです。





pointcut="org.springframework.samples.jpetstore.SystemArchitecture.businessService()"
advice-ref="txAdvice"/>

この定義は単に「businessService」の実行時に「txAdvice」によって参照されているアドバイスを実行しなければならないことを示しています。「businessService」ポイントカットはさきほど見た org.springframework.samples.jpetstore.SystemArchitectureアスペクトの中で定義されています。これはサービスインターフェース内で定義された全ての操作の実行にマッチします。

トランザクションアドバイスはそれ自身が多数のコンフィギュレーションを要するため、Sringはtx:advice要素をtxネームスペース中に提供して、これをよりシンプルそして明確にしています。jpetstoreアプリケーションのための「tx:advice」beanの定義がどのようになるか、次に示します。








アノテーションを使った更にシンプルなトランザクション設定の方法があります。@Transactionalアノテーションを使うときには、唯一、次のようなXMLが必要です。


アノテーションのアプローチを使うとPetServiceの実装は次のようにアノテートされます。:

/*
* all operations have TX_REQUIRED, default isolation level,
* read-write transaction semantics by default
*/
@Transactional
public class PetStoreImpl implements PetStoreFacade, OrderService {

...

/**
* override defaults on a per-method basis
*/
@Transactional(readOnly=true)
public Account getAccount(String username) {
return this.accountDao.getAccount(username);
}

...

}

WEB、サービス、データアクセス各レイヤの単純化

Spring AOPはWEB、サービス、データアクセス各レイヤを単純化するために利用することができます。このセクションでは二つの例を見ていきます。ひとつはデータアクセスレイヤから、もうひとつはサービスレイヤから抜き出したものです。

SpringのHibernateTemplateサポートクラスなしでHibernate3を使ってデータアクセスレイヤを実装すると仮定してください。あなたは今アプリケーションにSpringを使うことを計画しており、サービスレイヤでSpringのきめ細かい DataAccessExceptionヒエラルキーを活用したいと考えています。SpringのHibernateTemplateは HibernateExceptionを自動的にDataAccessExceptionに変換してくれますが、あなたが十分に満足している既存のデータレイヤ実装があるので、あなたは今それをSpringのサポートクラスをベースにしたものに書き直したいとは思っていません。これはあなたが例外の変換を自分で実装しなければならないということです。この要件を簡単にいうとこういうことです。

HibernateExceptionがデータアクセスレイヤからスローされた後、それが呼び出し元に渡される前にDataAccessExceptionに変換する。

AOPを使うと、この実装は上の要求記述と同じくらい簡単です。AOPを使わないでこれを実装することを考えると頭が痛くなるでしょう。「myapp」のためのHibernateExceptionTranslatorアスペクトがどのようになるか、次に示します。

@Aspect
public class HibernateExceptionTranslator {

private HibernateTemplate hibernateTemplate;

public void setHibernateTemplate(HibernateTemplate aTemplate) {
this.hibernateTemplate = aTemplate;
}

@AfterThrowing(
throwing="hibernateEx",
pointcut="org.aspectprogrammer.myapp.SystemArchitecture.dataAccessOperation()"
)
public void rethrowAsDataAccessException(HibernateException hibernateEx) {
throw this.hibernateTemplate
.convertHibernateAccessException(hibernateEx);

}

}

アスペクトは変換を実行するためにHibernateTemplateを必要とします。他のSpring beanと同じようにDIを使ってそれを設定しましょう。アドバイスの宣言はできれば要求記述を直接翻訳したものとして簡単に理解できるべきです。「@AfterThrowing a HibernateException(hibernateEx) from a dataAccessOperation(), rethrowAsDataAccessException」という具合に。シンプルに、そしてパワフルに!

ajc(AspectJコンパイラ)を使ってアプリケーションをビルドすることができます。ですが、ここでajcを使う必要はないのです。Spring AOPは@AspectJアスペクトを理解してくれるのですから。

application contextファイルに二つの設定が必要です。まず、@AspectJアスペクト型のbeanは全てSpring AOPプロキシを設定するために使われるべきであることをSpringに伝える必要があります。この設定をするのは一回だけで、次のような要素をapplication contextファイルの好きな場所に定義することで実現できます。

ここで、他の正規のSpring beanと同じように例外変換用のbeanを宣言して設定しなければなりません(ここにはAOP特有の設定はありません)。


class="org.aspectprogrammer.myapp.dao.hibernate.HibernateExceptionTranslator">





Spring AOPの設定には、beanのクラス(HibernateExceptionTranslator)が@AspectJアスペクトであるというわずかな事実だけで十分なのです。

完全を期すため、XMLのアスペクト宣言を使ってこれを行うにはどうすればよいかも見ていきましょう(例えばJDK1.4で作業しているときのため)。 HibernateExceptionTranslatorのためのbean定義は上述のものと変わりません。クラス自身はアノテートはもはや不要ですが、残りの部分はまったく同じです。

public class HibernateExceptionTranslator {

private HibernateTemplate hibernateTemplate;

public void setHibernateTemplate(HibernateTemplate aTemplate) {
this.hibernateTemplate = aTemplate;
}

public void rethrowAsDataAccessException(HibernateException hibernateEx) {
throw this.hibernateTemplate
.convertHibernateAccessException(hibernateEx);

}

}

これはもう@AspectJアスペクトではないので、aspectj-autoproxy要素を使うことはできません。代わりにXMLで次のようにアスペクトを定義します。





throwing="hibernateEx"
pointcut="org.aspectprogrammer.myapp.SystemArchitecture.dataAccessOperation()"
method="rethrowAsDataAccessException"/>


これが「after-throwing a hibernateEx from a dataAccessOperation, rethrowAsDataAccessException」と読めるところは、ひとつ前のバージョンとよく似ています。前に定義済みの hibernateExceptionTranslator beanを参照しているaop:aspect要素の「ref」属性に注目してください。rethrowAsDataAccessExceptionメソッドはこのbeanインスタンスに対して呼び出されます。hibernateExはメソッド上で宣言されているパラメータ(このケースでは唯一のパラメータ)の名前です。

ということで、私たちは要求を実装することができました(2回も!)。@AspectJ形式では15行のコード(空白行を除く)と1行のXMLを記述しました。データアクセスレイヤ全体にわたって一貫した正しい振る舞いを実現するにはこれで十分です。すこし大きいかもしれませんが。

この独自アスペクトのすばらしい利点のひとつは、もし後になってデータレイヤをJPA(EJB 3 persistence)ベースの実装(Hibernateや他のJPA実装を使う)に移行したくなっても、サービスレイヤはその影響を受けず、 DataAccessExceptionを使って動作を続けることが可能であることです(Springは、他のORM実装と同じようにJPAのためのテンプレートと例外変換機能を提供する予定です)。

これで私たちはサービスレイヤできめの細かいDataAccessExceptionを使った作業ができるわけですから、これを使って何か有益なことができます。特に、冪等(べきとう・同じデータに対して何度操作を行っても結果が同じになるような操作のことを冪等な操作という)な操作が例えばデッドロックの例外などで失敗したときに、ConcurrencyFailureExceptionをキャッチして透過的にリトライすることができます。並列処理に失敗した冪等なサービス操作が、エラーをクライアントに返す前に設定した回数だけ透過的にリトライする、という横断的な要件を実装してみましょう。

その仕事をしてくれるアスペクトを次に示します。

@Aspect
public class ConcurrentOperationExecutor implements Ordered {

private static final int DEFAULT_MAX_RETRIES = 2;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int order = 1;
private boolean retryOnOptimisticLockFailure = false;

/**
* configurable number of retries
*/
public void setMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
}

/**
* Whether or not optimistic lock failures should also be retried.
* Default is not to retry transactions that fail due to optimistic
* locking in case we overwrite another user's work.
*/
public void setRetryOnOptimisticLockFailure(boolean retry) {
this.retryOnOptimisticLockFailure = retry;
}

/**
* implementing the Ordered interface enables us to specify when
* this aspect should run with respect to other aspects such as
* transaction management. We give it the highest precedence
* (1) which means that the retry logic will wrap the transaction
* logic - we need a fresh transaction each time.
*/
public int getOrder() {
return this.order;
}

public void setOrder(int order) {
this.order = order;
}

/**
* For now, just assume that all business services are idempotent
*/
@Pointcut("org.aspectprogrammer.myapp.SystemArchitecture.businessService()")
public void idempotentOperation() {}

@Around("idempotentOperation()")
public Object doConcurrentOperation(ProceedingJoinPoint pjp)
throws Throwable {
int numAttempts = 0;
ConcurrencyFailureException failureException;
do {
try {
return pjp.proceed();
}
catch(OptimisticLockingFailureException ex) {
if (!this.retryOnOptimisticLockFailure) {
throw ex;
}
else {
failureException = ex;
}
}
catch(ConcurrencyFailureException ex) {
failureException = ex;
}
}
while(numAttempts++ < this.maxRetries);
throw lockFailureException;
}

}

もう一度述べますが、このアスペクトはSpring AOPでもAspectJでも変わることなく用いることができます。aroundアドバイス(doConcurrentOperation)は ProceedingJoinPoint型の特別なパラメータをとります。このオブジェクトに対してproceedメソッドが呼ばれると、そのアドバイスが取り囲んでいる(around)処理が実行されます。

全てのコメントと定型句であるgetterおよびsetterを取り除くと、このアスペクトの重要な部分はまだたったの32行です。設定ファイルにはすでにaspectj-autoproxy要素を記述しているので、あと必要なのは単なるbean定義を追加することだけです。


class="org.aspectprogrammer.myapp.service.impl.ConcurrentOperationExecutor">


では、サービスレイヤの全ての操作が冪等というわけではないとしたら? どうやって冪等なものを特定すればよいのでしょう? ここでポイントカット言語のパワーがものを言います。私たちは、冪等な操作を表わす次のような抽象概念をすでにもっています。

  @Pointcut("org.aspectprogrammer.myapp.SystemArchitecture.businessService()")
public void idempotentOperation() {}

もし冪等な操作であることを決定し表現する方法を変えたければ、やらなくてはならないのはポイントカットを変更することだけです。たとえば、 @Idempotentというマーカーアノテーションで冪等な操作を定義してもかまいません。ポイントカット式をIdempotentアノテーションをもっているビジネスサービスだけにマッチするように書き換えるのはとても簡単です。

  @Pointcut(
"org.aspectprogrammer.myapp.SystemArchitecture.businessService() &&
@annotation(org.aspectprogrammer.myapp.Idempotent)")
public void idempotentOperation() {}

APTを使うよりも少し簡単です。ポイントカットは「idempotentOperationはIdempotentアノテーションをもつbusinessService」であるとわかりやすく示しています。

サービス操作の大部分は冪等であることが望ましいです。そういうケースでは、冪等なものをピックアップするよりも冪等でないものをアノテートするほうが楽かもしれません。たとえば@IrreversibleSideEffectsのようなものでそれを実現します。これは、操作が技術的および精神的な作用をもたらすことを示します(絶対自分のコードを「IrreversibleSideEffects」でアノテートしたくはないですけどね。精神に作用しないように書き直すほうがいいです(^_-) )。idempotentOperationは一箇所にまとめて定義されているので簡単に変更できます。

  @Pointcut(
"org.aspectprogrammer.myapp.SystemArchitecture.businessService() &&
!@annotation(org.aspectprogrammer.myapp.IrreversibleSideEffects)")
public void idempotentOperation() {}

これでidempotentOperationはIrreversibleSideEffectsアノテーションをもたないbusinessServiceを指します。

開発時アスペクトによる生産性向上

Spring AOP用に@AspectJを記述することに慣れてきたら、次は、開発時にAspectJを使うだけでも(そして実行中のアプリケーションに AspectJでコンパイルされたアスペクトがなくても)、そこから更にたくさんの利益を得ることができます。アスペクトは、テストを行うとき(モックや障害のインジェクトを簡単にしてくれる)、問題をデバッグして原因をつきとめるとき、アプリケーション用に定めた設計ガイドラインが遵守されているか確認するときの手助けをしてくれます。

最初に、設計を強制するアスペクトの例を見てみましょう。データアクセスレイヤの作業を引き続きテーマにします。私たちは今Springの HibernateTemplateを導入し、Hibernateセッションの管理を自分たち自身で行わずSpringに任せたいと考えています。次に示すアスペクトはプログラマがうっかり自分でセッション管理を始めてしまわないよう保証してくれます。

public aspect SpringHibernateUsageGuidelines {

pointcut sessionCreation()
: call(* SessionFactory.openSession(..));

pointcut sessionOrFactoryClose()
: call(* SessionFactory.close(..)) ||
call(* Session.close(..));

declare error
: sessionCreation() || sessionOrFactoryClose()
: "Spring manages Hibernate sessions for you, " +
"do not try to do it programmatically";
}

このアスペクトがあると、プログラマがEclipseのAspectJ Development Tools (AJDT)(サイト・英語) プラグインを使っていれば、problemsビューとソースコード中の問題となっている箇所に、編集されたエラーマーカーが「Spring manages Hibernate sessions for you, do not try to do it programmatically」というエラーメッセージとともに表示されます(通常のコンパイルエラーと同じです)。この例のような強制用アスペクトを導入するお勧めの方法は、AspectJのコンパイルステップを、アプリケーションに強制用アスペクトを織り込むビルドプロセスに追加することです。アスペクトによってビルドエラーが見つかると、このタスクは失敗します。

次にシンプルな診断用アスペクトを見てみましょう。いくつかのトランザクションをread-onlyとしてマーク(重要なパフォーマンス最適化です)したことを思い出してください。アプリケーションが複雑になるにつれて、トランザクション境界があるサービスレイヤの操作からユースケースの一部として実行されるビジネスドメインのロジックまでの(概念的な)距離は長くなります。もし、ドメインロジックがread-onlyのトランザクション実行中にドメインオブジェクトの状態を更新したら、その更新は失われてしまう恐れがあります(データベースにはコミットされないので)。これは発見の難しいバグの原因になる可能性があります。

LostUpdateDetectorアスペクトは、開発時に、潜在的な更新の喪失を検出する手伝いをしてくれます。

public aspect LostUpdateDetector {

private Log log = LogFactory.getLog(LostUpdateDetector.class);

pointcut readOnlyTransaction(Transactional txAnn) :
SystemArchitecture.businessService() &&
@annotation(txAnn) && if(txAnn.readOnly());

pointcut domainObjectStateChange() :
set(!transient * *) &&
SystemArchitecture.inDomainModel();

..

アスペクトに二つの有用なポイントカットを定義することから始めます。readOnlyTransactionはreadOnlyプロパティがtrueにセットされた@TransactionalアノテーションをもつbusinessService()の実行(execution)を指します。また domainObjectStateChangeはinDomainModel()における非transientなフィールドの更新を指します。(これは単純化されてはいますが、それでも有用です。ドメインオブジェクトの状態が更新されるとはどういう状況をいうのかという視点から見ると、コレクションなどを扱うためにアスペクトを拡張することも可能です。もしそれが求められるのならですが。) 定義したこれら二つの概念を使って、 potentialLostUpdate()が何を意味するのかを次のように書き表すことができます。

  pointcut potentialLostUpdate() :
domainObjectStateChange() &&
cflow(readOnlyTransaction(Transactional));

potentialLostUpdate はreadOnlyTransaction実行中のコントロールフロー内で行われたdomainObjectStateの変更を指します。ここで、ポイントカット言語のパワーを見ることができます。二つの名前付ポイントカット式を組み合わせることでパワフルな概念を非常にシンプルに記述することができました。ポイントカット言語を用いることで、未熟なインターセプションモデルしか利用できないときに比べて、potentialLostUpdateのような条件の記述が一層簡単になります。また、EJB3が提供するもののように単純化しすぎたインターセプションメカニズムに比べて、はるかにパワフルです。

最後にもちろん、potentialLostUpdateが起こったときには実際に何かを行う必要があります。

  after() returning : potentialLostUpdate() {
logLostUpdate(thisJoinPoint);
}

private void logLostUpdate(JoinPoint jp) {
String fieldName = jp.getSignature().getName();
String domainType = jp.getSignature().getDeclaringTypeName();
String newValue = jp.getArgs()[0].toString();
Throwable t = new Throwable("potential lost update");
t.fillInStackTrace();
log.warn("Field [" + fieldName + "] in type [" + domainType + "] " +
"was updated to value [" + newValue + "] in a read-only " +
"transaction, update will be lost.",t);
}

}

以下は、このアスペクトを使ってテストを実行した結果得られたログエントリです。

WARN - LostUpdateDetector.logLostUpdate(41) | Field [name] in type
[org.aspectprogrammer.myapp.domain.Pet] was updated to value [Mr.D.]
in a read-only transaction, update will be lost.
java.lang.Throwable: potential lost update
at org.aspectprogrammer.myapp.debug.LostUpdateDetector.logLostUpdate(LostUpdateDetector.aj:40)
at org.aspectprogrammer.myapp.debug.LostUpdateDetector.afterReturning(LostUpdateDetector.aj:32)
at org.aspectprogrammer.myapp.domain.Pet.setName(Pet.java:32)
at org.aspectprogrammer.myapp.service.impl.PetServiceImpl.updateName(PetServiceImpl.java:40)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:100)
at org.aspectprogrammer.myapp.service.impl.ConcurrentOperationExecutor.doConcurrentOperation(ConcurrentOperationExecutor.java:37)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:478)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:344)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)

余談ですが、クリーンで読みやすいスタックトレース(それと最適化されたリトライのロジック)に注目してください。読みやすいスタックトレースは、例外スタックとレースエントリからノイズを取り除いてくれる別のアスペクトによるものです。スタックトレース管理アスペクトがなければ、Spring AOPのインターセプションスタックフレームが全て表示され、下に示すようなスタックトレースになります。単純化されたバージョンが大きな進歩であることに、きっと皆さんも同意してくれるでしょう。

WARN - LostUpdateDetector.logLostUpdate(41) | Field [name] in type
[org.aspectprogrammer.myapp.domain.Pet] was updated to value [Mr.D.]
in a read-only transaction, update will be lost.
java.lang.Throwable: potential lost update
at org.aspectprogrammer.myapp.debug.LostUpdateDetector.logLostUpdate(LostUpdateDetector.aj:40)
at org.aspectprogrammer.myapp.debug.LostUpdateDetector.ajc$afterReturning$org_aspectprogrammer_myapp_debug_LostUpdateDetector$1$b5d4ce0c(LostUpdateDetector.aj:32)
at org.aspectprogrammer.myapp.domain.Pet.setName(Pet.java:32)
at org.aspectprogrammer.myapp.service.impl.PetServiceImpl.updateName(PetServiceImpl.java:40)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:287)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:181)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:148)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:100)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:71)
at org.aspectprogrammer.myapp.service.impl.ConcurrentOperationExecutor.doConcurrentOperation(ConcurrentOperationExecutor.java:37)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:568)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:558)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:57)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:176)
at $Proxy8.updateName(Unknown Source)
at org.aspectprogrammer.myapp.debug.LostUpdateDetectorTests.testLostUpdateInReadOnly(LostUpdateDetectorTests.java:23)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at junit.framework.TestCase.runTest(TestCase.java:154)
at junit.framework.TestCase.runBare(TestCase.java:127)
at junit.framework.TestResult$1.protect(TestResult.java:106)
at junit.framework.TestResult.runProtected(TestResult.java:124)
at junit.framework.TestResult.run(TestResult.java:109)
at junit.framework.TestCase.run(TestCase.java:118)
at junit.framework.TestSuite.runTest(TestSuite.java:208)
at junit.framework.TestSuite.run(TestSuite.java:203)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:478)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:344)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:196)

構造の基礎をなす要件の実装の単純化

AspectJと付随ツールの使い心地が更によくなってきたら、今度はドメインモデルも含めてアプリケーションの全体に影響を及ぼすような要件の実装に AspectJを使いましょう。シンプルな例として、jpetstoreサンプルアプリケーションをプロファイルする方法をお見せします。まず Profilerアスペクトを見てから、その周囲を取り巻く詳細をいくつか埋めていきましょう。

public aspect Profiler {

private ProfilingStrategy profiler = new NoProfilingStrategy();

public void setProfilingStrategy(ProfilingStrategy p) {
this.profiler = p;
}

pointcut profiledOperation() :
Pointcuts.anyPublicOperation() &&
SystemArchitecture.inPetStore() &&
!within(ProfilingStrategy+);

Object around() : profiledOperation() {
Object token = this.profiler.start(thisJoinPointStaticPart);
Object ret = proceed();
this.profiler.stop(token,thisJoinPointStaticPart);
return ret;
}
}

profiledOperation()を、inPetStore()のanyPublicOperation()として定義しました。アスペクトは、 SpringでDIを使って設定するProfilingStrategyに処理を委譲するコントローラとして振る舞います。

  
class="org.springframework.samples.jpetstore.profiling.Profiler"
factory-method="aspectOf">






class="org.springframework.samples.jpetstore.profiling.JamonProfilingStrategy"
init-method="reset"
destroy-method="report">

アスペクトbeanに「factory-method」属性を使っていることに注意してください。これはシングルトンのAspectJアスペクトの設定と正規のSpring beanの設定の間にあるただ一つの相違点です。プロファイリングには、非常にシンプルなAPIを提供しているJAMon(サイト・英語)を使用しています。

public class JamonProfilingStrategy implements ProfilingStrategy {

public Object start(StaticPart jpStaticPart) {
return MonitorFactory.start(jpStaticPart.toShortString());
}

public void stop(Object token, StaticPart jpStaticPart) {
if (token instanceof Monitor) {
Monitor mon = (Monitor) token;
mon.stop();
}
}
}

ことで、プロファイリングの結果をWEBブラウザ上で見ることができます。しばらくの間アプリケーションのあちこちをクリックして回ったあとのスクリーンショットを貼っておきます。

ドメインモデルの単純化

ドメインモデルの複数個所に影響を及ぼすようなビジネス要件があることも珍しいことではありません。明白な例では、デザインパターンの実装(Nick Leseickiがこのトピックについて書いたすばらしい記事があるので見てください(part 1(source), part 2(source))。ドメインオブジェクトのDI(Springの@Configurableアノテーションの例として使っています)、ビジネスルールやポリシーの実装などが挙げられます。AOP導入のこの段階では、コアとなるビジネスロジックがアスペクトの存在に依存します。あなたが書いたアスペクトはあなたのドメインに特有のものです。AspectJとAJDTはどちらもAspectJを使って構築されており、私たちはそれらの構築の中で数多くのドメイン特有のアスペクトを使用します。例として、私が1.5.1リリースの開発期間中にAspectJに追加したアスペクトを挙げます。このアスペクトは、例外が空のキャッチブロックによって無視されてしまったときにlint警告を発行するというよく必要とされる機能を実装しています。

public aspect WarnOnSwallowedException {

pointcut resolvingATryStatement(TryStatement tryStatement, BlockScope inScope)
: execution(* TryStatement.resolve(..)) &&
this(tryStatement) &&
args(inScope,..);

after(TryStatement tryStatement, BlockScope inScope) returning
: resolvingATryStatement(tryStatement,inScope) {
if (tryStatement.catchBlocks != null) {
for (int i = 0; i < tryStatement.catchBlocks.length; i++) {
Block catchBlock = tryStatement.catchBlocks[i];
if (catchBlock.isEmptyBlock() ||
catchBlock.statements.length == 0) {
warnOnEmptyCatchBlock(catchBlock,inScope);
}
}
}
}

private void warnOnEmptyCatchBlock(Block catchBlock, BlockScope inScope) {
inScope.problemReporter()
.swallowedException(catchBlock.sourceStart(),
catchBlock.sourceEnd());
}
}

このアスペクトはこの例におけるコードベースの中の単一の場所にしかアドバイスを適用しませんが、それはJDTコンパイラの機能へのAspectJの追加をモジュール化することによってコードを一層クリアにし、将来のメンテナーにとってこの機能がどうやって実装されているかが一目瞭然になります。

アスペクトを使ったドメインモデリングの扱いの更なる詳細は別の記事のテーマです。

まとめ

Springはエンタープライズアプリケーション開発のためのシンプルかつパワフルなアプローチを提供することを目指しています。SpringがAOPをサポートしAspectJと統合したことで、このアプローチは、アプリケーションの複数個所に影響を及ぼす機能の実装にまで広がりました。従来のやり方だと、そのような機能の実装はアプリケーションロジックのいたるところにまき散らかされ、機能の追加、削除、メンテナンスを難しくし、アプリケーションロジックを複雑にしていました。アスペクトを用いると、Springはそのような機能のクリーンでシンプルでそしてモジュール性のある実装を記述可能にしてくれます。

AOPの導入はいくつもの段階を追って進めることができます。まずSpringが提供する追加設定の不要なアスペクトを活用するところからスタートし、次にSpring AOPを使って独自の@AspectJアスペクトWEB、サービス、データアクセス各レイヤに追加すればよいでしょう。AspectJ自体は、実行時の AspectJへの依存を持ち込まずに開発時の生産性を向上させるために使うことができます。更に一段踏み込むと、アプリケーションの複数レイヤにわたって影響を及ぼすような構造の基礎をなす要件をAspectJアスペクトを使って簡単に実装することができます。そして最後にはドメインモデル自体の実装を単純化するためにアスペクトを使うことができるようになります。

著者紹介

Adrian ColyerはInterface21のチーフ・サイエンティストで、Eclipse.orgのAspectJプロジェクトのリーダー、AspectJ Development Tools(AJDT)プロジェクトの創始者でもあります。2004年にはMITのTechnology Review誌でワールド・トップ・ヤング・イノベーター100人の中の一人に選ばれました。Spring、AOP、AspectJを題材によく講演を行います。

Interface21について

Interface21ではSpring、AOP、AspectJのトレーニングとコンサルティングを行っています。コーススケジュールや社内研修のアレンジについてはwww.interface21.comを参照してください。

2006年12月7日~10日のhttp://www.thespringexperience.comカンファレンスでAdrian Colyerや他のSpringコミュニティメンバーに会うことができます。

(編集部注:日本語訳掲載時点ではすでに開催済みです)

原文はこちらです:http://www.infoq.com/articles/Simplifying-Enterprise-Apps

(このArticleは2006年8月10日にリリースされました)


この記事に星をつける

おすすめ度
スタイル

BT