サンプルコード

JPetStore 6 は MyBatis 3, Spring 4 そして Stripes を使って構築された Web Application で、downloads セクションからダウンロードすることができます。 この章では、このサンプルを見ながら実用的な Web Application の構築方法と実行方法について説明します。

目的

この新しい JPetStore は、以前の版と同じ思想、つまり シンプルさ を重視しています。 JPetStore 6 は、僅かなクラスで完全な Web Application が構築可能で、そのために高度なコーディングスキルが必要な訳ではない、ということを証明するために開発されました。 必要なのは Java と SQL の基礎知識だけです。

eduardo@nomada ~ $ ./cloc-1.60.pl ~/git/jpetstore-6/src/main/
      60 text files.
      60 unique files.
       3 files ignored.

http://cloc.sourceforge.net v 1.60  T=0.28 s (209.8 files/s, 17722.9 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Java                            24            480            462           1429
JSP                             20            148              0            984
XML                              9             79            120            405
CSS                              1             46              0            277
SQL                              2             26             30            226
HTML                             2             44              0            143
-------------------------------------------------------------------------------
SUM:                            58            823            612           3464
-------------------------------------------------------------------------------
      	

6バージョン目となる今回の JPetStore は、最も小さいものとなりました。 設計の良さとプログラム構成を維持しながら、使用している Java クラスはたったの 24 です。 この後見ていきますが、JDBC の呼び出しやオブジェクトの生成、バインディングやトランザクション処理を行うコードはありません。 さらに凄いのは、MyBatis の API を呼び出すコードもないということです。 不思議に思われるかも知れませんが、このサンプルを見れば MyBatis の Mapper と Dependency Injection を組み合わせることで、MyBatis に依存しないアプリケーションを構築することができるということがお分かり頂けると思います。

プログラム構成

JPetStore 6 では、標準的な Maven プロジェクトのディレクトリ構成を踏襲しています。

/jpetstore                    <-- Maven pom.xml 
  /src
    /main/
      /java                   <-- Java コード
        /org/
          /mybatis
            /jpetstore
              /domain         <-- ドメインオブジェクト
              /mapper         <-- Mapper インターフェイス
              /service        <-- アプリケーションロジック
              /web
                /actions      <-- プレゼンテーションロジック(アクション)
      /resources              <-- Java 以外のファイル
        /org
          /mybatis
            /jpetstore
              /mapper         <-- Mapper XML ファイル
        /database
      /webapp
        /css
        /images
        /WEB-INF              <-- web.xml および applicationContext.xml
          /jsp                <-- JSP ファイル
      

設定ファイル

一連の設定ファイルはアプリケーション起動時に読み込まれます。 これらのファイルは、アプリケーションを構成する3つのフレームワークを設定するために必要です。 必要となるファイルは web.xml と applicationContext.xml の2つだけです。

web.xml

まずはじめに Stripes を開始する必要があるので、Stripes のマニュアルに従って設定していきます。 マニュアルによれば、Dispatcher Servlet と Filter を登録する必要があるようです。
早速やってみましょう。

<filter>
	<display-name>Stripes Filter</display-name>
	<filter-name>StripesFilter</filter-name>
	<filter-class>net.sourceforge.stripes.controller.StripesFilter</filter-class>
</filter>
<filter-mapping>
	<filter-name>StripesFilter</filter-name>
	<servlet-name>StripesDispatcher</servlet-name>
	<dispatcher>REQUEST</dispatcher>
</filter-mapping>
<servlet>
	<servlet-name>StripesDispatcher</servlet-name>
	<servlet-class>net.sourceforge.stripes.controller.DispatcherServlet</servlet-class>
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>StripesDispatcher</servlet-name>
	<url-pattern>*.action</url-pattern>
</servlet-mapping>

Stripes が ActionBean クラスを検出する際、検索対象となるパッケージを指定します。

<filter-class>net.sourceforge.stripes.controller.StripesFilter</filter-class>
  <init-param>
    <param-name>ActionResolver.Packages</param-name>
    <param-value>org.mybatis.jpetstore.web</param-value>
  </init-param>
</filter>

Stripes に関する設定は以上です。次に Spring の設定を行います。 Spring のマニュアルによれば、Spring を開始するために Context Listener を登録する必要があるようです。
早速やってみましょう。

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

特に指定しなければ、Spring は /WEB-INF/applicationContext.xml を設定ファイルとして使用します。デフォルトの設定で問題ないので次へ進みます。

Spring を使用することを Stripes にも教えておく必要があります。これによって、Spring Bean を直接 Stripes の ActionBean にインジェクトできるようになります。 再度 Stripes のマニュアルに従って Interceptor を登録します。

<filter-class>net.sourceforge.stripes.controller.StripesFilter</filter-class>
  ...
  <init-param>
    <param-name>Interceptor.Classes</param-name>
    <param-value>net.sourceforge.stripes.integration.spring.SpringInterceptor</param-value>
  </init-param>
</filter>

web.xml は完成です。気づいたかも知れませんが、MyBatis 3 に関してはまだ何も設定していません。 次のセクションで説明しますが、Spring の applicationContext.xml の方で行います。

applicationContext.xml

ご存知のように、applicationContext.xml は Spring の設定ファイルです。 依存性注入フレームワークである Spring に、どの Bean を生成し、どのように依存性を解決するかを指示するのが applicationContext.xml の役目です。詳しく見て行きましょう。

まずは、どこに Service Bean が配置されているか指定しましょう。Spring はクラスパス内を検索しますので、起点となるパッケージを指定するだけで OK です。

<context:component-scan base-package="org.mybatis.jpetstore.service" />

NOTE Spring のコンポーネントスキャン機能では、MyBatis の Mapper を検出することはできません。 Mapper は通常の Bean ではないので、どのように初期化すれば良いか Spring には分かりません。 すぐ後で説明するように、Mapper は MapperScannerConfigurer を使って生成します。

DataSourceTransactionManager も必要です。このアプリケーションはデモなので、HSQL のインメモリデータベースを作成して用意されたデータを流し込むテスト用の DataSource と、トランザクションのために標準的な DataSourceTransactionManager を使用します。

<jdbc:embedded-database id="dataSource">
  <jdbc:script location="classpath:database/jpetstore-hsqldb-schema.sql"/>
  <jdbc:script location="classpath:database/jpetstore-hsqldb-dataload.sql"/>
</jdbc:embedded-database>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>

ここまでは Stripes と Spring に関する標準的な設定のみでしたが、ここからは MyBatis に関する設定を行なっていきます。 このマニュアルで学んだように、MyBatis を Spring と連携させる場合、最低必要となるのは SqlSessionFactoryBean と Mapper クラスの2つです。 まずは SqlSessionFactoryBean を定義しましょう。

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
</bean>

次に Mapper の設定が必要ですが、ここでは Spring のコンポーネントスキャンと似たような機能を持った MapperScannerConfigurer を使います。 このクラスはクラスパスを検索して、検出した Mapper クラスを MyBatis に登録します。Spring のコンポーネントスキャンと同様、起点となるパッケージを指定する必要があります。

<mybatis:scan base-package="org.mybatis.jpetstore.mapper" />

Mapper XML ファイルを作成する際のタイピング量を減らすため、Bean のエイリアスを使いましょう。 SqlSessionFactoryBeantypeAliasPackage で Bean の検索対象を指定しておくと、短縮名で Bean を参照できるようになります。

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="typeAliasesPackage" value="org.mybatis.jpetstore.domain" />
</bean>

これで設定は完了ですが、実行する前にコード全体の構成を確認してみましょう。

コードの概要

JPetStore 6 は典型的な MVC アプリケーションで、プレゼンテーション層、ビジネスロジック層、データアクセス層の3つで構成されています。

プレゼンテーション層

プレゼンテーション層は、JSP ファイルと Stripes の ActionBean で構成されています。 JSP で使っているのは HTML、JSTL タグ、Stripes タグのみなので、このサンプルに関しては特筆すべきことはありません。 Stripes の ActionBean は Struts のアクションや Spring MVC のコントローラーに相当するクラスで、これについても特別なことはありません。

Stripes と Spring を連携できるように設定したので、Service を ActionBean にインジェクトすることができます。これによって生成や参照取得について考えなくても Service を利用することができます。 例として、CatalogActionBean を見てみましょう。

@SessionScope
public class CatalogActionBean extends AbstractActionBean {
  ...
  @SpringBean
  private transient CatalogService catalogService;
  ...
  public ForwardResolution viewCategory() {
    if (categoryId != null) {
      productList = catalogService.getProductListByCategory(categoryId);
      category = catalogService.getCategory(categoryId);
    }
    return new ForwardResolution(VIEW_CATEGORY);
  }
  ...

@SpringBean アノテーションによって、Spring に登録されている Bean が ActionBean に注入されます。

ロジック層

アプリケーションロジック層は、サービスとして動作する Plain な Java Bean と、ドメインとして動作する Plain な Java Bean によって構成されています。 この層では、データベースから取得したデータをドメインオブジェクトに設定する処理と、ドメインオブジェクトのデータを使ってデータベースを更新する処理を担当しています。 そのため、この層はトランザクショナル、つまり複数のデータベース更新処理をまとめて実行できるようになっている必要があります。

これをどのようにして実現しているのか、OrderService を見てみましょう。

@Service
public class OrderService {

  @Autowired
  private ItemMapper itemMapper;
  @Autowired
  private OrderMapper orderMapper;
  @Autowired
  private LineItemMapper lineItemMapper;

  @Transactional
  public void insertOrder(Order order) {
    order.setOrderId(getNextId("ordernum"));
    for (int i = 0; i < order.getLineItems().size(); i++) {
      LineItem lineItem = (LineItem) order.getLineItems().get(i);
      String itemId = lineItem.getItemId();
      Integer increment = new Integer(lineItem.getQuantity());
      Map<String, Object> param = new HashMap<String, Object>(2);
      param.put("itemId", itemId);
      param.put("increment", increment);
      itemMapper.updateInventoryQuantity(param);
    }

    orderMapper.insertOrder(order);
    orderMapper.insertOrderStatus(order);
    for (int i = 0; i < order.getLineItems().size(); i++) {
      LineItem lineItem = (LineItem) order.getLineItems().get(i);
      lineItem.setOrderId(order.getOrderId());
      lineItemMapper.insertLineItem(lineItem);
    }
  }

まず気づくのは、サービスなのに JDBC 関連のコードや MyBatis 関連のコードがないということです。 DAO パターンを使っていてデータベースアクセスのコードは DAO 側に記述してあるのだろう、と思うかも知れませんが、後で見るようにデータアクセス層は通常の Java インターフェイスである MyBatis Mapper で構築されています。 こうした構成にしておくことで、アプリケーション全体を通して MyBatis API の呼び出しが不要となっているのです。

次に目立つのは、コミットやロールバックの記述がないという点でしょう。 これは MyBatis-Spring がサポートする Spring の宣言的トランザクションを使っているためです。 Spring の @Transactional アノテーションは、このメソッドがトランザクション管理の対象であり、updateInventoryQuantity, insertOrder, insertLineItem の一連の呼び出しが全て成功しない限り、コミットされないことを表しています。

パーシステンス層

パーシステンス層は MyBatis の Mapper によって構成されています。 Mapper は Plain な Java インターフェイスと SQL ステートメントを含む Mapepr XML ファイルです。 この層には独自の Java コードはありません。 OrderMapper インターフェイスの getOrder メソッドが呼ばれると、MyBatis によって OrderMapper.xml ファイルで定義されている getOrder SQL ステートメントが実行され、データベースから取得したデータが格納された Order ドメインオブジェクトが生成されます。

public interface OrderMapper {
  List<Order> getOrdersByUsername(String username);
  Order getOrder(int orderId);
  void insertOrder(Order order);
  void insertOrderStatus(Order order);
}
<mapper namespace="org.mybatis.jpetstore.mapper.OrderMapper">

  <cache />

  <select id="getOrder" resultType="Order" parameterType="int">
    SELECT
      BILLADDR1 AS billAddress1,
      BILLADDR2 AS billAddress2,
      BILLCITY,
      BILLCOUNTRY,
      BILLSTATE,
      BILLTOFIRSTNAME,
      BILLTOLASTNAME,
      BILLZIP,
      SHIPADDR1 AS shipAddress1,
      SHIPADDR2 AS shipAddress2,
      SHIPCITY,
      SHIPCOUNTRY,
      SHIPSTATE,
      SHIPTOFIRSTNAME,
      SHIPTOLASTNAME,
      SHIPZIP,
      CARDTYPE,
      COURIER,
      CREDITCARD,
      EXPRDATE AS expiryDate,
      LOCALE,
      ORDERDATE,
      ORDERS.ORDERID,
      TOTALPRICE,
      USERID AS username,
      STATUS
    FROM ORDERS, ORDERSTATUS
    WHERE ORDERS.ORDERID = #{value}
      AND ORDERS.ORDERID = ORDERSTATUS.ORDERID
  </select>
  ...
</mapper>

NOTE Mapper XML ファイルに <cache /> 要素を追加することで、簡単にクエリ結果のキャッシュを有効化することができます。 お望みであれば Spring を使ってより高いレベルで Mapper や Service メソッドへの呼び出し結果をキャッシュさせることもできます。

JPetStore の実行

本当に動くの?と思うかも知れません。もちろんです! 実際に試してみましょう。/

クリーンなコンピューターを持っているとして、Eclipse 上の Tomcat を使ってサンプルを実行する手順を下記に示します。

これで戯れる準備ができました。自由に変更を加えていろいろ試してみてください。

不具合や不備、改善点など(例えばテストケースが無いとか!)を見つけたら、変更内容の diff ファイルをパッチにして トラッカー にチケットを作成してください。よろしくお願いします!

NOTE JPetStore 6 は Servlet 2.5 と JSP 2.1 をサポートする Java Server で動作します。 NetBeans や Eclipse も必須ではありません。お好きな IDE やコマンドラインで実行することができます。