Spring Frameworkは、コンテナによってBeanのインスタンスの生成や関連付けを管理するDIコンテナです。Spring Frameworkでは、POJO(Plain Old Java Object)といわれる、コンテナやフレームワークが提供する特定のクラスを継承したり、特定のインターフェースを実装する必要がない単純なJavaBeansをコンテナ上で管理します。また、トランザクション管理やAOPなどのコンテナが提供する機能を、設定によりコンテナ上のBeanに追加することができます。このような特徴により、Spring FrameworkではPOJOを組み合わせて強力なアプリケーションを作成することができます。
ただし、Spring FrameworkにBeanを登録したり、コンテナが提供する機能を利用するためには、いろいろと設定をしなければいけません。Spring Framewrokでは、XMLを用いてこの設定を記述するのですが、この記述はプログラマにとってあまりエキサイティングなものではなく、特に多くのBeanを利用するようなアプリケーションでは、設定ファイルが巨大になるため、その見通しの悪さや記述の冗長性から、XMLで記述する設定ファイルは、Spring Frameworkを利用する上で多くのプログラマが不平を言うポイントのひとつでした。
当初のSpring Frameworkでは、XML以外に設定を記述する方法がなかったのですが、Spring Frameworkのバージョンも2.0、2.5と進むうちに、様々な設定をアノテーションを用いて記述できるようになりました。ここでは、XMLを用いて設定をしていたアプリケーションを、アノテーションを用いた方式に書き換えることにより、どれだけXMLの記述量を減らせるか、どのようなところをアノテーションで記述し、どのようなところをXMLで記述する必要があるかといったポイントを検証していきます。
今回のサンプルアプリケーション
Webアプリケーションは、大きく分けると、Web層のコンポーネント、サービス層のコンポーネント、データアクセス層のコンポーネントで構成されています。ここでは、サンプルアプリケーションを用いて、それぞれの層でアノテーションを活用し、どのように設定を簡略化できるかを説明していきます。
ここで、今回利用するサンプルアプリケーションを説明します。このアプリケーションは、ユーザ名とパスワードを入力し、ログインボタンを押すと、その値を元に、ログイン成功/失敗を判定し、その結果から成功もしくは失敗ページを表示する非常に単純なアプリケーションです。アプリケーションの内部をもう少し詳しく説明します。ページ上のフォームに入力された値をWeb層での窓口となるログインコントローラが受け取り、そのデータを使ってログイン関連のサービスを受け持つログインサービスを呼び出しします。ログインサービスはメンバー情報をデータベースから取得するメンバーDAOを利用してログインの可否を判定します。メンバーDAOはO/RマッピングツールのHibernateを利用してデータベースとやり取りします。これらのコンポーネントの結びつけをSpring Frameworkが受け持っています。
図:サンプルアプリケーションの概要
今回は、サービス層とデータアクセス層、次回は、Web層を説明していきます。ですので、今回は、ログインサービスとメンバーDAOを中心に見ていきます。
データアクセスオブジェクト (XML設定ファイル版)
はじめに、メンバーDAOのソースコードを説明します。メンバーDAOのクラス名をMemberDaoHibernateImplとします。このクラスはMemberDaoというインターフェースを実装したクラスです。サービス層のコンポーネントからは、このインターフェースを経由してアクセスすることとなります。このクラスはデータベースにアクセスする際に、Hibernateを利用していますので、HibernateへのインターフェースとなるSessionFactoryを使います。このSessionFactoryは、Spring Frameworkによってこのクラスに注入されます。そして、このSessionFactoryを使って、HibernateTemplateというSpring Frameworkが提供するHibernateアクセス用に便利な機能を提供するオブジェクトを作成します。
ここでは、findByLoginnameというログイン名を元にユーザを検索するメソッドを実装していますが、これ以外にも通常データアクセスオブジェクトには、CRUD(作成、取得、更新、削除)を担当するメソッドが作成されますが、ここでは割愛します。
package sample.dao;
…
public class MemberDaoHibernateImpl implements MemberDao {
private static final String FIND_BY_LOGINNAME_HQL =
"FROM MemberData WHERE loginname = ?";
private HibernateTemplate hibernateTemplate;
public void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
}
public MemberData findByLoginname(String loginname) {
List result =
this.hibernateTemplate.find(FIND_BY_LOGINNAME_HQL,
loginname);
if (result.size() > 0) {
return (MemberData) result.get(0);
} else {
return null;
}
}
…
}
サービスオブジェクト (XML設定ファイル版)
次はログインサービスのソースコードです。このクラスのクラス名は、LoginServiceImplとし、LoginServiceインターフェースを実装しています。Web層のコンポーネントなどからは、このインターフェースを経由してアクセスされることとなります。また、データアクセスにメンバーDAOを利用していますので、このインスタンスはコンテナによって注入されます。
このクラスには、ログインの処理を受け持つloginメソッドが実装されています。
package sample.service;
…
public class LoginServiceImpl implements LoginService {
private MemberDao memberDao;
public MemberDao getMemberDao() {
return memberDao;
}
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
public boolean login(String loginname, String password) {
MemberData member = memberDao.findByLoginname(loginname);
if (member != null) {
if (password.equals(member.getPassword())) {
return true;
}
}
return false;
}
}
設定ファイル (XML設定ファイル版)
次に、Spring Frameworkの設定ファイルを説明します。Spring Frameworkの設定ファイルは、XMLで記述します。
このファイルでは、最初にサービスコンポーネントとデータアクセスオブジェクトの登録をしています(1)。ここでは、ログインサービスとメンバーDAOの登録と、それぞれが参照しているメンバーDAOとSessionFactoryを設定しています。
次に、このアプリケーションが利用するリソースの設定をしています(2)。ここでは、データベースへの接続を受け持つデータソースとHibernateへのインターフェースとなるSessionFactoryを設定しています。
最後に、トランザクションの設定をしています(3)。ここでは、サービスコンポーネント(sample.serviceパッケージの中でServiceImplで終わるクラス)にトランザクションの設定をしています。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<!-- (1)サービスコンポーネント、DAOの登録 -->
<bean id="loginService" class="sample.service.LoginServiceImpl">
<property name="memberDao" ref="memberDao" />
</bean>
<bean id="memberDao" class="sample.dao.MemberDaoHibernateImpl">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<!-- (2)リソースの設定 -->
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="org.h2.Driver" />
<property name="url" value="jdbc:h2:tcp://localhost/test" />
<property name="username" value="sa" />
<property name="password" value="" />
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mappingResources">
<list>
<value>sample/data/MemberData.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.show_sql=true
</value>
</property>
</bean>
<!-- (3)トランザクションの設定 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" />
<tx:method name="login" read-only="true" />
<tx:method name="*" />
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceOperation"
expression="execution(* sample.service.*ServiceImpl.*(..))" />
<aop:advisor advice-ref="txAdvice"pointcut-ref="serviceOperation" />
</aop:config>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
このアプリケーションでは、あまり多くのBeanを利用していないため、設定ファイルがあまり複雑ではありませんが、アプリケーションの規模が大きくなるにつれ、Beanの登録や参照の設定など記述する項目が増えていきます。また、トランザクションなどの設定もAOPを利用してパターンマッチを利用したりして一括に登録することができますが、個々のメソッドごとにReadOnlyなどのトランザクション属性を細かく指定しようとすると、記述量や複雑性が増していきます。このように、Spring Frameworkは、アプリケーションの規模が大きくなると、XMLの記述量が増大し、設定全体の見通しが悪くなるという傾向がありました。
アノテーションを利用した設定
ここからは、ここまでで説明したアプリケーションを動作を変えずに、アノテーションを用いることにより、全体の設定がどのように変わるか見ていきましょう。
データアクセスオブジェクト (アノテーション利用版)
まず、データアクセスオブジェクトに変更を加えます。元々のコードに、まずクラスレベルのアノテーションとして、@Repositoryアノテーションを加えます。このアノテーションは、このクラスがデータベースなどとやり取りをするクラスであることを表します。これによって、Spring Frameworkがこのクラスを自動的にコンテナが管理するBeanであることを認識します。それに加えて、このクラスのメソッドから発するデータアクセスに関連する実行時例外が、Spring Frameworkが提供するデータアクセス関連の例外であるDataAccessExceptionに自動的に変換されます。現在の実装では、Hibernate、TopLink、JPA、JDOなどの例外が変換されます。これによって、処理する例外をまとめられるため、このBeanを利用する他のBeanの例外のハンドルが容易になります。
また、このBeanでは、Hibernateへアクセスするため、SessionFactoryを利用します。そのため、これを保持するプロパティのsetterメソッドに、@Autowiredというメソッドレベルのアノテーションを付与しています。こうすることにより、コンテナが管理しているBeanの中からこのプロパティの型にあうBeanを自動的にこのフィールドに注入します。このアノテーションのrequired属性にtrueが設定されていますので、何も注入されないと例外が発生します。
package sample.dao;
…
@Repository
public class MemberDaoHibernateImpl implements MemberDao {
…
@Autowired(required=true)
public void setSessionFactory(SessionFactory sessionFactory) {
this.hibernateTemplate = new HibernateTemplate(sessionFactory);
}
…
}
サービスオブジェクト (アノテーション利用版)
次にサービスオブジェクトに変更を加えます。このコンポーネントは、サービスレイヤーのコンポーネントなので、クラスレベルの@Serviceアノテーションを付与しています。このアノテーションによって、Spring Frameworkがこのクラスを見つけ出して、自動的にコンテナが管理するBeanとして登録します。ここでは@Serviceアノテーションを用いましたが、@Componentアノテーションでも同様にこのクラスをコンテナが管理するBeanとして登録することができます。現状では、この@Serviceアノテーションを付与しても@Componentアノテーションを付与した場合と違いはありません。しかし、このコンポーネントがサービス層のコンポーネントを明確にする意味でも、サービス層のコンポーネントには@Serviceアノテーションを用いるべきでしょう。今後のリリースでは、@Serviceアノテーションを付与したBeanには自動的にトランザクション管理の機能が付与されるなどの機能拡張も期待されます。また、このアノテーションの値として、loginServiceという値を設定していますが、これは他のBeanがこのBeanを名前で参照する場合に用いられる名前を指定するものです。これを指定しないとデフォルトではパッケージを含まないクラス名の一文字目を小文字にしたものになります。例えばこのクラスにアノテーションで名前を指定しなかった場合は、Bean名はloginServiceImplとなります。
また、このクラスでは、メンバーDAOを利用しますので、このプロパティのsetterメソッドに@Autowiredアノテーションを付与しています。これによってこのプロパティに自動的にメンバーDAOのインスタンスが注入されます。
このコードでは、プロパティの型によってコンテナがどのBeanを注入するかを決定します。しかし、同じ型のインターフェースを実装したBeanが複数ある場合など、コンテナが自動的に決定できない場合があります。そのような時は、以下のように@Qualifierアノテーションを引数に付与し、名前を指定することによって、どのBeanを注入するか特定することができます。
@Autowired(required=true)
public void setMemberDao(@Qualifier("memberDao") MemberDao memberDao) {
this.memberDao = memberDao;
}
通常、サービス層のコンポーネントを単位にトランザクションを管理します。そのため、このクラスのクラスレベルアノテーションとして@Transactionalアノテーションを付与します。これによって、このクラスのメソッドを実行する前にトランザクションを開始し、実行が終わるとトランザクションを終了するようになります。また、loginメソッドでは、データベースの変更はしないため、読み込みのみのトランザクションで問題がありません。そこで、このメソッドのメソッドレベルアノテーションにも@Transactionalアノテーションを付与し、readOnly属性にtrueを設定しています。このようにクラスレベルに全体的なトランザクション設定をして、その設定よりもさらに細かく設定したい時は、個々のメソッドにトランザクション属性を指定したアノテーションを付与することにより、クラス全体の設定を上書きすることができます。
package sample.service;
…
@Service("loginService")
@Transactional
public class LoginServiceImpl implements LoginService {
…
@Autowired(required=true)
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
…
@Transactional(readOnly=true)
public boolean login(String loginname, String password) {
…
}
}
設定ファイル (アノテーション利用版)
最後に、設定ファイルに変更を加えます。
この設定ファイルでは、最初にアノテーションを付与したコンポーネントを検索する設定をしています(1)。ここで指定したパッケージに属しているクラスを検索し、そこに@Repository、@Service、@Controller、@Componentといったアノテーションが付与されたクラスがあった場合は、それがBeanとしてコンテナに登録されます。複数のパッケージを検索したい場合は、カンマ区切りで列挙します。ここで検索されたBeanに@Autowiredなどで他のBeanを参照している場合は、コンテナによって解決されて、対応するプロパティが注入されます。
次に利用する外部リソースの設定をします(2)。ここは元の設定と変わらず、データソースとSessionFactoryの設定がされています。
最後にトランザクションの設定をします(3)。ここでの設定により、読み込まれたBeanに@Transactionalアノテーションが付与されていた場合に、その設定に従ってトランザクションが管理されるようになります。元々の設定では、ここでどのクラスのどのメソッドにどのようなトランザクション属性を付与するかなどを細かく設定する必要がありましたが、ここではその必要はなく、個々のクラスでアノテーションで指定された設定が適用されます。
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
">
<!-- (1)アノテーションを付与したBeanを読み込む設定 -->
<context:component-scan base-package="sample.dao,sample.service"/>
<!-- (2)外部リソースの設定 -->
<bean id="dataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="org.h2.Driver" />
<property name="url" value="jdbc:h2:tcp://localhost/test" />
<property name="username" value="sa" />
<property name="password" value="" />
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="mappingResources">
<list>
<value>sample/data/MemberData.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.show_sql=true
</value>
</property>
</bean>
<!-- (3)トランザクションの設定 -->
<tx:annotation-driven transaction-manager="txManager"/>
<bean id="txManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
XMLによる設定とアノテーションを利用した設定の違い
以上で、従来のXML設定ファイルですべてを設定する方式からアノテーションを利用して設定する方式にアプリケーションを書き換えました。今回はアプリケーションの規模が小さいのでXML設定ファイルの記述量が減ったとあまり感じられないかもしれません。しかし、アプリケーションの規模が大きくなるとその違いは明確になると思います。ここで注目するには、XML設定ファイルに残った設定です。アノテーションを利用した場合にXML設定ファイルに残った設定は、データソースやSessionFactoryなどの外部リソースの設定です。つまり、アプリケーションを作る上で作成したBeanの登録や関連付けはアノテーションが受け持ち、データソースなどの外側のリソースに接続するための設定はXML設定ファイルが受け持つというように、登録するBeanの特性によって役割を分担することができます。
アノテーションを用いることにより、XML設定ファイルの記述量は減ると思います。各コンポーネントにアノテーションを書くことによって、自動的にコンテナがBeanを登録してくれるため、Bean一つ一つをXML設定ファイルに記述する必要がなくなるため、効率よくアプリケーションを開発することができるでしょう。しかし、設定情報が各クラスに分散するため、一元管理できなくなることは否めません。どちらの設定方法にも利点と欠点がありますので、開発チームやアプリケーションの特性を考慮して、どちらかの方式を選択する必要があるでしょう。
今回は、Spring Frameworkが提供するアノテーションを用いて、アプリケーションのサービス層のコンポーネントとデータアクセス層のコンポーネントを書き換えてみました。次回は、アノテーションを用いてWeb層のコンポーネントを書き換えてみようと思います。
著者について
河村 嘉之(かわむら かずゆき)ウルシステムズ株式会社所属
メーカー系SI会社にてJavaを用いたシステム開発、新技術評価を担当後、現在はウルシステムズにてオープンソースに関連するビジネスに従事。
日本Springユーザ会、日本Javaユーザ会などでも中心メンバーとして活動中。