レベル: 上級 夷藤 勇人, ソフトウェア事業,
IBM
2005年 09月 07日 Lightweightコンテナを使用したJ2EE開発は、その柔軟性・開発生産性を武器に、今日のJ2EEを席巻しつつあります。その根底を支えるのは、DI(Dependency Injection: 依存性注入)+AOP(Aspect Oriented Programming:アスペクト指向プログラミング)という強力なパラダイムです。
この記事は、従来の"重い"J2EE開発に対して疑問を感じている方、その開発生産性に対して日々不満を抱いている方たちが対象です。クラシックJ2EEアーキテクチャーからの脱却を願う開発者にとっての新たなる希望「Lightweightコンテナ・アーキテクチャー」を紹介します。
- この記事は、個々のフレームワーク・ツールの詳細な使用法・技術解説が目的ではありません。
- この記事は、ビジネスロジックレイヤーに焦点をあてます。Web・UIレイヤーは対象外とします。
- 特に注釈がない限り、この記事でEJBという場合は、J2EE 1.4までの、EJB 1.1からEJB 2.1、特にステートレス・セッションビーンのことを指します。
クラシックJ2EEアーキテクチャーとは
クラシックJ2EEアーキテクチャーに基づいたアプリケーションの代表例としては、J2EE開発者にとってはおなじみの“PetStore”があげられます。J2EE Blueprintsで使用されているサンプルアプリケーションで、J2EE開発者にとってお手本となるべく作成されました。ビジネスロジックレイヤーの実装にEJBを多用しているのが主な特徴です。
ここ数年にわたる、J2EEコミュニティーからのアンチEJBムーブメントにより、この元祖“クラシック”Petstoreは、格好の非難の的となってきました。
- 「J2EEデザインパターンといいつつ実はアンチパターン集」
- 「必要のないオーバー・エンジニアリング」
なかでもクラシックJ2EEアーキテクチャーでもっとも槍玉にあげられたのは、EJBを全面的に使用している点です。
テスタビリティー(Testability : テスト容易性)は、アジャイル開発に代表される・今日のソフトウェア開発における最重要要素です。これは、J2EE開発プロジェクトにおいても例外ではありません。J2EE開発プロジェクトにおいて、テスタビリティーが阻害されるのは、なんといってもEJBを使用した時です。当たり前のことですが、EJBはEJBコンテナ内でしか動作しません。さらに悪いことに、EJBコンポーネントは取り除くことが困難な多くの外部要因に依存しがちです。
テスト・ドリブンな開発を進める上で、このような「テストのやりづらさ」は致命的な障壁となります。ユニットテストの本来の目的とは関係のない外部要素を全て用意しておかないとテストを実行できません。
EJB is Old Fashioned?
少し歴史を振り返ってみましょう。EJB 1.0の仕様がはじめて制定されたのは、1998年のことです。当時の状況と比べて、今日ではJavaはいくつかの重要な進化をとげています。
- J2SE 1.3におけるDynamic Proxy(動的プロクシ/java.lang.reflect.Proxy)の導入。これにより、事前の静的なコード生成の必要性が大幅になくなりました。
- CGLIBに代表される、実行時バイトコード生成テクノロジーの成熟。
- JVMにおける世代別GC(ガベージ・コレクション)の実装。これにより、小さなオブジェクトの生成・回収に関しては、ほとんどそのコストを意識する必要はなくなりました。
EJBはこれらの恩恵をまったく受けることのできなかった時代に制定されたものです。当時の状況にもとづいた思想・制限をいまだに引きずっている面があります。
- 事前のデプロイメント・コード生成を前提とした開発モデル
今では、Dynamic Proxy、あるいは実行時コード生成の使用を前提にした開発モデルになるでしょう。こちらの方がよりスマートで、開発者に余分な負担をかけません。
- 分散オブジェクトが前提
EJB 2.0からはローカル・インターフェースが導入されましたが、分散オブジェクトが前提という負のプログラミング・モデルは相変わらず残っています。必要のないところに「分散」をもちこむのは悪以外の何者でもありません。
- 1インスタンス1スレッドの強制
たとえば、Servletを考えてみましょう。Servletは標準で1インスタンスを複数スレッドで使用するモデルです。SingleThreadModelはもはや非推奨になりました。しかし、ステートレス・セッションビーンはあいかわらず、1インスタンスは1スレッドのみという強制から逃れるすべはありません。たとえ、スレッドセーフなステートレス・セッションビーンを作成したとしてもです。Servletでは非推奨になったはずのSingleThreadModelのみしか選択肢が開発者には与えられないのです。
- インスタンスプールモデル
JVMがオブジェクトの生成・回収が苦手だったころでは納得できる考え方でした。しかし、今日のモダンなJVM上では生成コストが無視できるようなステートレスなインスタンスをプールしておく意義はもはや存在しません。
- JNDIに代表されるDependency Lookupにもとづく依存オブジェクトの取得
今日では、Dependency Injection(依存性注入: DIについては後述)を用いて、依存性を解決するのがスマートです。
EJBの複雑な開発モデルは、開発者に無駄な負担をかけています。
もっと楽をしましょう。
いつまでもひとつのモデルに縛られる必要はありません。J2EEコミュニティーから生まれたイノベーションの恩恵を我々は受けてもよいはずです。EJBを使用しなければ不可能だったことが、EJBを使用しなくても・より"Lightweight"な方法で実現できる時代です。
Lightweightコンテナ・アーキテクチャー
Lightweightコンテナ・アーキテクチャーは、J2EEコミュニティーから生まれたイノベーションを武器とした、“軽量”かつ“柔軟”なアプローチです。基本となる設定思想は、古き良きただのJavaのオブジェクト・「POJO(Plain
Old Java Object)」への回帰です。
EJBのように、特定のインターフェース・クラスの実装・継承を強制されることは良しとしません。それでは、せっかく作成したコンポーネントが特定の環境に依存してしまいます。
Lightweightコンテナ・アーキテクチャーでは、ビジネスロジックの実装であるビジネス・オブジェクトはPOJOで実装します。
様々なサービス・特定の環境への依存性などは、POJO内にはあらかじめ書いておきません。実行時に動的にPOJOに組み込みます。
EJBがEJBコンテナに管理されるのと同様に、POJOはLightweightコンテナによって管理されます。コンテナというと特別な印象を持ってしまうかもしれませんが、実態はただのPure
Javaなライブラリーです。あらゆるJava環境から利用できます。
今回は、最初にJava単体、すなわちJ2SE環境で、Lightweightコンテナを使用したサンプルを動かします。J2EEアプリケーションサーバーは必要としません。
その後、今度は、コードを一切変更しないでそのままJ2EEアプリケーションサーバー環境上で同様に動作させてみます。
POJOベースのO/Rマッピング
まず、Lightweightコンテナ・アーキテクチャーの真髄である、DI + AOPの説明をする前に、ビジネスロジックレイヤーを実装する上で不可欠なデータパーシステンスの実装の話をします。
今回のサンプルでは、ORM(Object Relational Mapping)ツールとして、標準の地位となりつつあるHibernateを使用しています。例として、「Customer」エンティティーの定義を示します。
POJOベースのエンティティー定義:
import javax.persistence.*;
...
@Entity
public class Customer {
private Integer id;
private String name;
Set<Order> orders = new HashSet<Order>();
@Id(generate=GeneratorType.AUTO)
public Integer getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@OneToMany(mappedBy="customer", cascade=CascadeType.ALL,
fetch=FetchType.EAGER)
public Set<Order> getOrders() {
return orders;
}
public void setOrders(Set<Order> orders) {
this.orders = orders;
}
|
Hibernateを使用する場合は、別途設定ファイルにO/Rマッピング情報を定義するのが一般的ですが、今回は、アノテーションを使用して直接ソースコード上にマッピング情報を定義します。
使用しているアノテーションは、「JSR220-EJB 3.0 Persistence API」で定義されているアノテーションです。「Persistence
API」は、もともとはEJB 3のために規定されていたものですが、EJBコンテナ外でも使用できるようにと、APIが独立しました。パッケージ名も、ずばりjavax.persistence.*です。
これを受けて、HibernateではO/Rマッピングの定義に、Persistence APIで規定されているアノテーションを使用できるようになりました。
プログラミング to インターフェース
インターフェースに対してプログラミングするというのは、柔軟なアーキテクチャーにとっての基本です。今回の、ビジネスロジックの構成を図1に示します。
図1 ビジネスロジックレイヤー
クライアントに公開するインターフェースとして、AppServiceを用意しています。AppSerivceImplがその実装です。CustomerDao,
OrderDaoといったDAO(Data Access Object)を使用してビジネスロジックを実装しています。各DAOの実装クラスは、HibernateのSessionFactoryを使用しています。
Dependency Injectionとは?
インターフェースに対してプログラミングするのがよいというのは、誰もが理解しているところです。ところが、実際にはインターフェースの積極的使用に関してはちょっとした心理的抵抗があります。「そのインターフェースの実装クラスをどうやって取得する?」という根本的な問題があるからです。
ビジネスロジックの実装クラスであるAppServiceImplを例にとってみましょう。このクラスは、インターフェースOrderDaoとCustomerDaoを使用して、ビジネスロジックを実装しています。
AppServiceImplの立場になって考えてみましょう。各DAOの実装クラスを、どうやって入手すればよいでしょうか?
インターフェースの実装クラスを直接指定する:
public class AppServiceImpl implements AppService {
CustomerDao customerDao = new CustomerDaoImpl();
....
|
このように直接コードに書いてしまっては、CustomerDaoの具象クラス、CustomerDaoImplに依存してしまいます。
それでは、ファクトリーを用いて実装クラスを隠蔽するようにしてみてはどうでしょうか?
ファクトリー経由でインターフェースの実装クラスを取得する:
public class AppServiceImpl implements AppService {
CustomerDao customerDao = AdHocFactory.getCustomerDao();
....
|
確かに、実装クラスは隠蔽することができました。その代わり、今度はファクトリーへの依存性がひとつ追加されてしまいました。AppServiceImplは単にCustomerDaoを使用したいだけなのです。ファクトリーの使用方法など知りたくはありません。本来の責務外です。
そこで、登場するのがDependency Injection - DIです。DIの考え方は、こうです。
オブジェクト自身は、依存オブジェクトを宣言するだけです。自分からは探しにいきません。個々のオブジェクトがそのようなことを気にする必要はないのです。
その代わり、外部から「依存オブジェクト(Dependency)を注入(Injection)」してもらうのです。どのように依存性を注入するかについてはいろいろ方法があります。一例として、以下のようにセッターメソッド経由で、依存オブジェクトを受け取る方法があります。
依存性の宣言 - セッターメソッドの提供:
public class AppServiceImpl implements AppService {
CustomerDao customerDao;
....
public void setCustomerDao(CustomerDao customerDao) {
this.customerDao = customerDao;
}
|
適切な依存オブジェクトをパラメーターとしてセッターメソッドをコールすることによって依存オブジェクトを注入できます。
このようにセッターメソッドを用意しておけば、以下のように外部から各オブジェクトの依存オブジェクトを注入できます。
依存オブジェクトの注入:
DataSource dataSource = .....;
SessionFactory sessionFactory = .... // dataSourceを使用;
CustomerDaoImpl customerDao = new CustomerDaoImpl();
customerDao.setSessionFactory(sessionFactory);
OrderDaoImpl orderDao = new OrderDaoImpl();
orderDao.setSessionFactory(sessionFactory);
AppServiceImpl appServiceTarget = new AppServiceImpl();
appServiceTarget.setCustomerDao(customerDao);
appServiceTarget.setOrderDao(orderDao);
|
しかし、依存性の解決をこのようにハードコーディングしていては、あまりエレガントではありません。各オブジェクトの依存関係を、「設定ファイル」で記述できればより高いレベルの柔軟性を手にすることができます。
そこで登場するのが、DIコンテナです。今回は、DIコンテナの代表例として、Springフレームワークを使用します。Springフレームワークを使用した場合の、設定ファイルは以下のようになります。
Springフレームワークによる設定ベースの依存性注入:
<bean id="myDataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName"
value="org.hsqldb.jdbcDriver"/>
...
</bean>
<bean id="mySessionFactory"
class=".....AnnotationSessionFactoryBean">
....
<property name="dataSource" ref="myDataSource"/>
....
</bean>
<bean id="myAbstractDao" abstract="true">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
<bean id="myCustomerDao"
class="com.example.CustomerDaoImpl"
parent="myAbstractDao"/>
<bean id="myOrderDao" class="com.example.OrderDaoImpl"
parent="myAbstractDao"/>
<bean id="myAppServiceTarget"
class="com.example.AppServiceImpl">
<property name="customerDao" ref="myCustomerDao"/>
<property name="orderDao" ref="myOrderDao"/>
</bean>
|
この設定を受け、以下のように依存オブジェクトが注入されます。
- データソースを定義(id: myDataSource)
- HibernateのSessionFactoryに1.のデータソースを注入(id: mySessionFactory)
- CustomerDaoImplに2.のSessionFactoryを注入(id:myCustomerDao)
- OrderDaoImplに2.のSessionFactoryを注入(id:myOrderDao)
- AppServiceImplに3.と4.の各DAO実装を注入(id:myAppServiceTarget)
DIが解決しようとしている事柄があまりに根本的なことなので、最初はなかなかDIのメリットが理解できないかもしれません。ユニットテストの重要性が最初はなかなか理解できなかったことを思い出してください。
まるでレゴ・ブロックを組み立てるかのように、依存性の解決を一貫した設定ファイルベースで行えるという事実は、想像以上の快適さ・柔軟性を与えてくれます。
AOP(Aspect Oriented Programming)による宣言的トランザクション
EJBが提供する宣言的トランザクションサービスは、非常に魅力的です。プログラミング・モデルの大幅に簡略化につながるもので、その恩恵の大きさは、EJBの採用を正当化できる理由の一つです。
しかし、宣言的トランザクションは、もはやEJBの専売特許ではありません。DI
+ AOPの恩恵を受けることによって、POJOベースで実現可能です。今回は、やはり特別な環境を用意することなく、「ただのJ2SE環境」で、宣言的トランザクションを実現します。
AOPの出番です。
Springフレームワーク・AOPによる宣言的トランザクションの織り込み:
<beans>
<bean id="myTxManager"
class="org.springframework.orm.hibernate3.
HibernateTransactionManager">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
<bean id="myAppService"
class="org.springframework.transaction.interceptor.
TransactionProxyFactoryBean">
<property name="transactionManager" ref="myTxManager"/>
<property name="target" ref="myAppServiceTarget"/>
<property name="transactionAttributes">
<props>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
|
targetで指定されているid - myAppServiceTargetとは、実際のビジネスロジック実装であるAppServiceImplを指しています。このPOJOに対して宣言的トランザクションサービスというアスペクトを織り込んでいます。この例では、すべてのメソッドのトランザクション属性を"Required"に設定しています。
これで、全ての準備が整いました。ビジネスロジックのクライアントは、DIコンテナからPOJOを取り出して使用することになります。
DIコンテナからオブジェクトの取得:
ApplicationContext context = ...
AppService appService = (AppService) context.getBean("myAppService");
// call business logic
....
|
テスト容易性(Testability)の確保
作成したPOJOベースのビジネス・オブジェクトのテストを考えてみましょう。
ビジネス・オブジェクトのテスト:
import junit.framework.TestCase;
....
public class AppServiceTest extends TestCase {
private AppService appService;
protected void setUp() throws Exception {
super.setUp();
ApplicationContext context = ...
appService = (AppService) context.getBean("myAppService");
}
public void testCreateCustomer() {
Customer customer1 = new Customer();
customer1.setName("skywalker");
appService.createCustomer(customer1);
assertNotNull("id is given", customer1.getId());
Customer newCustomer = appService.loadCustomer(customer1.getId());
assertNotNull("created customer is found", newCustomer);
assertEquals("custemer's name was set",
"skywalker", newCustomer.getName());
|
これはなんの変哲もない、古き良きただのJUnitテスト・POJU(Plain Old JUnit
test)です。ビジネス・オブジェクトのユニットテストを実施するにあたって、アプリケーションサーバーや特別な細工はもはや必要ありません。
EJBのテスタビリティーに悩まされていた人にとって、POJOがもたらすテスタビリティーは救世主です。
非侵略性(non-invasiveness)
今回、作成したビジネスロジックレイヤーの各インターフェース・クラスを振り返ってみましょう。これらは、SpringフレームワークのAPIを一切使用していません。
EJBコンポーネントがEJBのAPI(javax.ejb.*)にひどく侵略されてしまい、EJBコンテナ以外では使用できなくなってしまうのと比較してみてください。この非侵略性(non-invasiveness)はDIコンテナを使用した場合の大きな利点のひとつです。
アプリケーションサーバー環境でPOJOにサービス
先ほどは、J2SE環境で動作させる例でした。実際のところ、ほとんどの人の関心は、最終的にはJ2EEアプリケーションサーバー環境で動かすことが目的でしょう。せっかくアプリケーションサーバー環境で動作させるのですから、J2SE環境では得ることのできないアプリケーションサーバーが提供するサービスを利用してみましょう。
- アプリケーションサーバーが提供するデータソースを利用したい
- 2フェーズコミットに参加したい
等が例としてあげられるでしょう。
この場合でも、POJO自体を変更する必要はありません。必要なことは設定の変更
- POJOに注入するものを変更- するだけです。
アプリケーションサーバー提供のサービスを利用する:
<bean id="myDataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc/abc" />
</bean>
<bean id="myTxManager"
class="org.springframework.transaction.jta.JtaTransactionManager"/>
|
この設定により、データソースとしては"java:comp/env/jdbc/abc"でJNDIルックアップして取得したものを、トランザクションマネージャーとしては"java:comp/UserTransaction"でJNDIルックアップして取得できるJTAサービスを使用したものを、利用することになります。
JTAの恩恵を受けることにより、グローバルトランザクションに参加・2フェーズコミットが可能になる等、J2EEアプリケーションサーバーのみが提供する機能をPOJOは享受します。
POJOのコード自体は動作環境が異なっても、変更する必要はありません。これがDIのもたらす柔軟性の威力です。
Java EE 5へ向けて
J2EEの次バージョン・Java EE 5はPOJOベースの開発モデルを採用しました。これは、J2EEコミュニティーからの非難を受け入れ、遅ればせながら現代のトレンドを大幅にとりいれた結果です。そのため従来のEJB非難の大部分はEJB 3にはあてはまらなくなります。これはよい流れです。
EJB 3はSpringフレームワークのコンセプトからも多大な影響を受けています。DIのコンセプトを取り入れ、依存リソースは宣言ベースで注入してもらうようになりました。
将来、Java EE 5が利用できるようになり、EJB 3のセッションビーンを使用したい場合はどうすればよいでしょうか?心配はありません。
今回、作成したPOJOはそのまま利用できます。必要な変更は、EJB 3用のアノテーション(@javax.ejb.Stateless)をPOJOに付与するだけです。
POJOのEJB 3 - ステートレス・セッションビーン化:
import javax.ejb.Stateless;
@Stateless
public AppServiceImpl implements AppService {
|
これでPOJOはJava EE 5のEJBコンテナ内で動作するときはステートレス・セッションビーンとして稼働するようになります。EJBコンテナが提供する宣言的トランザクションの恩恵を享受できます。
アノテーションがひとつ追加されたからといって、ただのPOJOであることには変わりません。J2SE環境では、DI+AOPを受けて宣言的トランザクションの恩恵をこれまでと同様に享受できます。テスタビリティーは失われません。この柔軟性は、EJB 3がPOJOベースの開発モデルを採用してくれたおかげです。
EJBを利用する/しないは、もはやアプリケーション・アーキテクチャーの絶対的な決定要素ではないのです。コードの全面的な変更を意味するものではなく、むしろ設定で変更可能な「実装上の選択要素」となるのです。「DI
+ AOP」によるパラダイムシフトです。クラシック世代にとどまっていては実現不可能な柔軟性を我々は得ることができるのです。
.NET開発者へ
DI + AOPという強力なパラダイムは、もはや、Java/J2EEの世界だけの特権ではなくなりつつあります。よい考え方・アーキテクチャーは、プラットフォームの垣根を越えて広がります。そう、.NETの世界にもです。
Java/J2EEの世界で生まれたSpringフレームワークですが、現在、その.NET版、Spring.Netが開発中です。.NET開発者も近い将来、この恩恵を享受することになるのでしょう。
最後に
POJOには本来集中するべきビジネスロジックのみを記述、必要な依存オブジェクトはDIコンテナから注入、横断的サービスは実行時に動的に提供してもらうというのは、これからのJ2EE開発が向かう道として既に標準となりつつあります。
Java EE 5が開発現場に浸透・利用できるようになるまでの数年の間、クラシック世代にとどまりつづける必要はあるのでしょうか?POJOベースのJ2EE開発を実践するには、いまそこにあるLightweightコンテナ・アーキテクチャーを採用するだけです。それが来るべきJava
EE 5時代への幕開けにもつながるのです。
ダウンロード | 内容 | ファイル名 | サイズ | ダウンロード形式 |
|---|
| サンプルソースコードのダウンロード1 | sample.zip | 6.08KB | HTTP |
|---|
注 - サンプルコードは、全面的にJ2SE 5.0の機能をとりいれて書いていますが、Lightweightコンテナ・アーキテクチャー自体は、J2SE 5.0が前提ではありません。J2SE 1.4環境でも利用可能です。
参考文献
著者について  | |  | 夷藤 勇人 , ソフトウェア事業 |
記事の評価
|