[Java] Spring to Java EE Migration, Part 3

原文はこちら。
http://www.oracle.com/technetwork/articles/java/springtojavaee3-1569377.html

はじめに
第1部第2部では、JavaServer Faces(JSF) 2.0、Enterprise  JavaBeans (EJB) 3.1、およびSpringのPet ClinicアプリケーションのMySQLスキーマからJava Persistence API(JPA)2.0を使って、完全なJava EEアプリケーションを生成しました。数回クリックするだけで、Springのサンプルアプリケーションが提供するものと同等の機能を持つアプリケーションを作成することができました。

また、生成されたコードを分析し、素晴らしいのJSFの機能(Faceletsテンプレート、データモデル、およびコンバータの機能)だけでなく、いくつかの先進的なJPAの機能(例えば、最大文字列長やフィールドのNULL可否といった情報を保持しつつもデータベース表の再生成時に使えるアノテーション属性)を有していることに注目しました。また、Bean Validationもサポートしています。

第3部では、よりユーザーフレンドリーにするため、アプリケーションを調整します。生成されたアプリケーションは、ページの一部で主キーを表示しており、これらのキーは代理キー、つまりビジネス上の意味がなく、一意の識別子として厳密に使用されるものゆえ、ユーザーの目に入れる理由はありません。さらに、よりユーザーフレンドリーにするために生成されたラベルの一部を変更します。

ユーザーインターフェースの調整
次のスクリーンショットは生成されたアプリケーションのイメージです。


いくつか改善できる点があります。はじめに、一般的なタイトルを変更しましょう。
<h:head>
        <title>Facelet Title</title>
        <h:outputStylesheet name="css/jsfcrud.css"/>
</h:head>
JSF 2.0の<h:head>タグはHTMLの<head>タグに類似しています。以下のように<title>タグ内のテキストを変更するだけでページのタイトルを変更できます。
<title>Pet Clinic</title>
このページを変更する一方で、もっと簡単な修正を施すこともできます。<h1>タグをページタイトルに付け、さらに各リンクの文字をよりわかりやすい言葉に変更します。
こうした変更の結果、アプリケーションはこんな感じになりました。

アプリケーション内を見ていくと、いくつかのラベルは明らかにJPAエンティティの対応するプロパティから生成したということがわかります。例えば、Show All Owner Items(原文ではDisplay All Owners)のリンクをクリックするとデータベースにあるペットの飼い主の全レコードを表示します。

上記表を生成するページのマークアップを調べると、これらのラベル(本件については他のほとんどのラベルが)bundleというリソースバンドルから取得していることがわかります。
姓と名のヘッダがFirstNameおよびLastNameあることに注意してください。こうなったのは、これらのラベルが、JPAのOwnerエンティティの対応する属性から作成されたためです。
コード1:ラベルのリソースバンドル
<h:dataTable value="#{ownerController.items}" var="item" border="0"
             cellpadding="2" cellspacing="0"
             rowClasses="jsfcrud_odd_row,jsfcrud_even_row"
             rules="all" style="border:solid 1px">
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListOwnerTitle_id}"/>
        </f:facet>
        <h:outputText value="#{item.id}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListOwnerTitle_firstName}"/>
        </f:facet>
        <h:outputText value="#{item.firstName}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListOwnerTitle_lastName}"/>
        </f:facet>
        <h:outputText value="#{item.lastName}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListOwnerTitle_address}"/>
        </f:facet>
        <h:outputText value="#{item.address}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListOwnerTitle_city}"/>
        </f:facet>
        <h:outputText value="#{item.city}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListOwnerTitle_telephone}"/>
        </f:facet>
        <h:outputText value="#{item.telephone}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="&nbsp;"/>
        </f:facet>
        <h:commandLink action="#{ownerController.prepareView}"
                       value="#{bundle.ListOwnerViewLink}"/>
        <h:outputText value=" "/>
        <h:commandLink action="#{ownerController.prepareEdit}"
                       value="#{bundle.ListOwnerEditLink}"/>
        <h:outputText value=" "/>
        <h:commandLink action="#{ownerController.destroy}"
                       value="#{bundle.ListOwnerDestroyLink}"/>
    </h:column>
</h:dataTable>
アプリケーションのfaces-config.xmlファイルを調べると、"bundle"はBundle.propertiesというプロパティファイルにマッピングされていることがわかります。
コード2:ラベルの変更
<faces-config version="2.0"
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd">
    <application>
        <resource-bundle>
            <base-name>/Bundle</base-name>
            <var>bundle</var>
        </resource-bundle>
    </application>
</faces-config>
Bundle.propertiesのラベルを編集して(長くなるので割愛します)、アプリケーションをよりユーザーフレンドリーにすることができます。このファイルをいくつか簡単に修正した後、Ownersページは次のようになります。

コード3:Id列を表から削除
<h:dataTable value="#{ownerController.items}" var="item"
             border="0" cellpadding="2" cellspacing="0"
             rowClasses="jsfcrud_odd_row,jsfcrud_even_row"
             rules="all" style="border:solid 1px">
    <h:column>
        <f:facet name="header">
            <h:outputText
                value="#{bundle.ListOwnerTitle_id}"/>
        </f:facet>
        <h:outputText
            value="#{item.id}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText
                value="#{bundle.ListOwnerTitle_firstName}"/>
        </f:facet>
        <h:outputText value="#{item.firstName}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText
                value="#{bundle.ListOwnerTitle_lastName}"/>
        </f:facet>
        <h:outputText value="#{item.lastName}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText
                value="#{bundle.ListOwnerTitle_address}"/>
        </f:facet>
        <h:outputText value="#{item.address}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText
                value="#{bundle.ListOwnerTitle_city}"/>
        </f:facet>
        <h:outputText value="#{item.city}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText
                value="#{bundle.ListOwnerTitle_telephone}"/>
        </f:facet>
        <h:outputText value="#{item.telephone}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="&nbsp;"/>
        </f:facet>
        <h:commandLink
            action="#{ownerController.prepareView}"
            value="#{bundle.ListOwnerViewLink}"/>
        <h:outputText value=" "/>
        <h:commandLink
            action="#{ownerController.prepareEdit}"
            value="#{bundle.ListOwnerEditLink}"/>
        <h:outputText value=" "/>
        <h:commandLink
            action="#{ownerController.destroy}"
            value="#{bundle.ListOwnerDestroyLink}"/>
    </h:column>
</h:dataTable>
この簡単な変更の結果、Owners Listのページは以下のようになります。

Id列を表から削除しましたが、飼い主のデータを作成したり編集するとIdフィールドは表示されてしまいます。
コード4:IdフィールドをCreateページから削除
<h:panelGrid columns="2">
    <h:outputLabel value="#{bundle.CreateOwnerLabel_id}" for="id" />
    <h:inputText id="id" value="#{ownerController.selected.id}"
                 title="#{bundle.CreateOwnerTitle_id}" required="true"
                 requiredMessage="#{bundle.CreateOwnerRequiredMessage_id}"/>
    <h:outputLabel value="#{bundle.CreateOwnerLabel_firstName}"
                   for="firstName" />
    <h:inputText id="firstName" value="#{ownerController.selected.firstName}"
                 title="#{bundle.CreateOwnerTitle_firstName}" />
    <h:outputLabel value="#{bundle.CreateOwnerLabel_lastName}" for="lastName" />
    <h:inputText id="lastName" value="#{ownerController.selected.lastName}"
                 title="#{bundle.CreateOwnerTitle_lastName}" />
    <h:outputLabel value="#{bundle.CreateOwnerLabel_address}" for="address" />
    <h:inputText id="address" value="#{ownerController.selected.address}"
                 title="#{bundle.CreateOwnerTitle_address}" />
    <h:outputLabel value="#{bundle.CreateOwnerLabel_city}" for="city" />
    <h:inputText id="city" value="#{ownerController.selected.city}"
                 title="#{bundle.CreateOwnerTitle_city}" />
    <h:outputLabel value="#{bundle.CreateOwnerLabel_telephone}"
                   for="telephone" />
    <h:inputText id="telephone" value="#{ownerController.selected.telephone}"
                 title="#{bundle.CreateOwnerTitle_telephone}" />
</h:panelGrid>
太字で強調した行を削除した結果、createページは以下のようになります。

しかし、JPAのOwnerエンティティのIdフィールドは@NotNullアノテーションがついているので、もう新しい所有者を作成することはできません。
@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@Basic(optional = false)@NotNull@Column(name = "id", nullable = false)private Integer id;
これを簡単に修正する場合、@NullアノテーションをIdフィールドから削除します。これで、明示的に主キーを入力しなくても新しいペットの飼い主を作成することができます。

新たに作成した飼い主情報を保存すると、Owner表に表示されます。

NetBeans組み込みのデータベースツールを使ってデータベース内に上記データを確認することもできます。

JPAの主キージェネレータが自動的にIdフィールドに値を追加しました。

こうして、自動生成されたラベルを修正するだけでなく、不要な入力フィールドを削除したり、利用者にとって不要な情報を隠したりして、生成したアプリケーションのユーザビリティを向上させました。

アプリケーションの表の多くは1対多のリレーションがあります。リレーションの"1"側の情報を見る場合、"多"側の情報の一部を見ることができます。例えば、ペットと飼い主には1対多のリレーションがあります(ペットの飼い主は一人ですが、飼い主には複数のペットを紐付けることができます)。システム中の全てのペットの情報を表示するページを見てみましょう。

TypeとOwner列の値が、リレーション先の主キー(外部キー)に対応する数値になっていることに注目して下さい。このページを修正して数字の代わりに文字で表現すべきでしょう。
ペットリストを生成するページのマークアップにはこうした値の取得方法を示しています。
コード5:Type列、Owner列の値を取得する
<h:dataTable value="#{petController.items}" var="item" border="0"
cellpadding="2" cellspacing="0"
rowClasses="jsfcrud_odd_row,jsfcrud_even_row"
rules="all" style="border:solid 1px">
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListPetTitle_name}"/>
        </f:facet>
        <h:outputText value="#{item.name}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListPetTitle_birthDate}"/>
        </f:facet>
        <h:outputText value="#{item.birthDate}">
            <f:convertDateTime pattern="MM/dd/yyyy" />
        </h:outputText>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListPetTitle_type}"/>
        </f:facet>
        <h:outputText value="#{item.type}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="#{bundle.ListPetTitle_owner}"/>
        </f:facet>
        <h:outputText value="#{item.owner}"/>
    </h:column>
    <h:column>
        <f:facet name="header">
            <h:outputText value="&nbsp;"/>
        </f:facet>
        <h:commandLink action="#{petController.prepareView}"
        value="#{bundle.ListPetViewLink}"/>
        <h:outputText value=" "/>
        <h:commandLink action="#{petController.prepareEdit}"
        value="#{bundle.ListPetEditLink}"/>
        <h:outputText value=" "/>
        <h:commandLink action="#{petController.destroy}"
        value="#{bundle.ListPetDestroyLink}"/>
    </h:column>
</h:dataTable>
列にはPetクラスのTypeプロパティとOwnerプロパティを意味する文字列を表示します。論理的に、こうしたオブジェクトのtoString()メソッドはIdを返すことを考えるでしょう。OwnerのtoString()の実装を見てみましょう。
@Override
public String toString() {
    return "com.ensode.petclinicjavaee.entity.Owner[ id=" + id + " ]";
}
おわかりのように、toString()がIDを返しません。何が起こっているのでしょう。IDをページに表示する理由はNetBeansのウィザードが全てのJPAエンティティ用のJSFコンバータを自動的に生成したためです。OwnerのコンバータはOwnerController.javaの内部クラスとして定義されています。
コード6:Ownerエンティティコンバータの定義
@FacesConverter(forClass = Owner.class)
public static class OwnerControllerConverter implements Converter {
    public Object getAsObject(FacesContext facesContext, UIComponent component, String value) {
        if (value == null || value.length() == 0) {
            return null;
        }

        OwnerController controller =
                (OwnerController) facesContext.getApplication().
                getELResolver().
                getValue(facesContext.getELContext(), null,
                "ownerController");
        return controller.ejbFacade.find(getKey(value));
    }

    java.lang.Integer getKey(String value) {
        java.lang.Integer key;
        key = Integer.valueOf(value);
        return key;
    }

    String getStringKey(java.lang.Integer value) {
        StringBuffer sb = new StringBuffer();
        sb.append(value);
        return sb.toString();
    }

    public String getAsString(FacesContext facesContext, UIComponent component, Object object) {
        if (object == null) {
            return null;
        }

        if (object instanceof Owner) {
            Owner o = (Owner) object;
            return getStringKey(o.getId());
        } else {
            throw new IllegalArgumentException("object " + object +
                    " is of type " + object.getClass().getName() +
                    "; expected type: " + OwnerController.class.getName());
        }
    }
}
ここでの関心はgetAsString()メソッドで、これはgetStringKey()メソッドを呼び出すものです。これは単にOwnerオブジェクトのIdプロパティを文字列に変換し飼い主のIDを表す文字列を返すメソッドです。これを飼い主の姓名を返すようにコードを変更することができます(コード7)。
コード7:飼い主の姓名を返す
public String getAsString(FacesContext facesContext,
        UIComponent component, Object object) {
    if (object == null) {
        return null;
    }

    if (object instanceof Owner) {
        Owner o = (Owner) object;
        return new StringBuilder(o.getFirstName()).append(
                   " ").append(o.getLastName()).toString();
    } else {
        throw new IllegalArgumentException("object " + object +
                " is of type " + object.getClass().getName() +
                "; expected type: " + OwnerController.class.getName());
    }
}
この変更に加え、全ての自動生成されたコンバータに変更を加えると、ペットリストのページは以下のようになります。

これでアプリケーションの微調整はほとんど終わりましたが、もう一つ残っています。1対多の関係を持つエンティティを修正したり作成する場合、多の関係にある値を選択するためのドロップダウンが現れます。例えば、Edit PetページにはTypeとOwnerのドロップダウンリストがあります。
[訳注]
NetBeans 7.1.2ではドロップダウンリストではなく、入力フィールドのみでしたが、原文のまま日本語にしています。

ドロップダウンリストに表示される文字はユーザフレンドリーではありません。各々の対応するエンティティの、生成されたtoString()メソッドが返した値を表示しています。生成された全てのtoString()メソッドを調べ、オブジェクトの適切な表現を返すように編集すると、ドロップダウンリストをよりユーザフレンドリーにすることができます。

適切なtoString()メソッドにちょっと変更を加えると、Edit Petのページは以下のようになります。

これで、完全に機能的かつユーザフレンドリーなPet Clinicアプリケーションができました。Spring版の移植版ではありませんが、同等の機能を持っています。Java EE版では獣医や獣医の専門を管理することができますが、Spring版ではできません。対してSpringバージョンでは飼い主や来歴を単一ページに表示しますが、Java EE版ではこうしたエンティティを別々に管理し、表示します。それにも関わらずアプリケーションはほぼ同等の機能を有しています。

Spring版とJava EE版のPet Clinicを比較してみる
Java EE版Pet Clinicアプリケーションの修正が終了したので、Spring版と比較してみましょう。

ファイルサイズと必要なライブラリ
まず生成されたアプリケーションのサイズからです。Java EE版は2MB以内に収まっています。
図14 Java EEのwarファイル

対して、Spring版では17GBあります。

Spring版はJava EE版の9倍以上のサイズですが、この主因はSpringアプリケーションはTomcatのようなServletコンテナにデプロイされることが通例であり、トランザクションやO-Rマッピング機能などを実装した様々なライブラリをインクルードする必要があるからです。Java EEアプリケーションサーバであれば、こうしたサービスは標準機能で提供されています。

ではSpring版のWEB-INF/libディレクトリを見てみましょう。

Spring版にはアプリケーションにバンドルする必要がある34個ものライブラリが含まれています。

では、Java EE版ではどうなっているでしょうか。

Java EE版にはWEB-INF以下にライブラリはありません。全てアプリケーションサーバが機能を提供してくれているので、外部ライブラリは不要なのです。

完全に公平を期すために、Spring版ではデータベースアクセスで3個の方法(Hibernate、JPA、JDBC)をサポートしています。対してJava EE版ではJPAのみをサポートしていることに着目すべきでしょう。しかし、Springアプリケーションの場合Java EEアプリケーションに比べて依存性の個数がかなり大きいということがポイントです。

設定ファイル
Spring版、Java EE版のアプリケーションを開発する際に必要なライブラリの個数を調べたので、次に必要な設定ファイルの個数に着目しましょう。

Spring版では、以下のXML設定ファイルを使っています。
ファイル名行数
context.xml7
web.xml141
applicationContext-jpa.xml101
Geronimo-web.xml5
applicationContext-hibernate.xml89
petclinic-servlet.xml68
petclinic.hbm.xml74
applicationContext-jdbc.xml81
総行数566

Java EEの場合はどうでしょうか。
ファイル名行数
web.xml24
faces-config.xml16
persistence.xml8
総行数48

Spring版の設定ファイルの行数は、Java EE版のほぼ12倍です。再度公平を期すため、これらのファイルの一部は、データアクセスのための特定のAPIを選択したり、特定のアプリケーションサーバにデプロイする場合に必要とされるものです。Tomcatにデプロイし、Hibernateを使ってデータアクセスをする場合には、geronimo-web.xml、applicationContext-jpa.xml、applicationContext-jdbc.xmlが不要になり、総行数で462になりますが、それでもJava EE版に比べてほぼ10倍の行数が必要です。
[訳注]
原文では462とありますが、単純計算すると379です。379の場合はほぼ7.5倍です。

コードとマークアップ
ファイルサイズ、依存性、設定ファイルの行数を比較してきましたが、次は実際のコードの量をSpring版、Java EE版で比較してみましょう。

Spring版のJSP、CSS(Cascading Style Sheet)、Javaのコード行数は2664行で、Java EE版のXHTML、CSS、Javaのコード行数は3768行です。
Spring版の行数がかなり少ないのですが、いくつか注意しておくことがあります。

Java EE版はSpring版をそのまま移植したわけではありません。例えば、Java EE版では獣医や獣医の専門を追加、更新、削除する機能がありますが、Spring版には獣医およびその専門を閲覧する機能しかありません。さらに、Spring版は単一ページで飼い主、来歴を管理および閲覧することができますが、Java EE版ではこうしたエンティティ各々を別のページで管理しています。
その他、Java EE版では多くのコードやマークアップを実際には書かなかったことを覚えておきましょう。これらはほとんどNetBeansのウィザードで作成されたものだからです。

これで第3部を終了します。次回はJava EEとSpringのAPIを比較します。

参考

著者について
David Heffelfinger
ソフトウェアコンサルタンティングファームであるEnsode Technology, LLCのCTO。
1995年よりアーキテクチャ、デザイン、ソフトウェア開発に従事。1996年からJavaを主要なプログラミング言語として活用し、大規模プロジェクト(アメリカ国土安全保障省、Freddie Mac、Fannie Mae、アメリカ国防総省など)に参画。Southern Methodist Universityソフトウェア工学修士。

0 件のコメント:

コメントを投稿