[Java] Spring to Java EE Migration, Part 2

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

はじめに
第1部では、SpringのPet ClinicサンプルアプリケーションをJava EEで完全に書き換えはじめました。アプリケーションの永続化レイヤはJava Persistence API (JPA) 2.0で開発しましたが、NetBeansを使うとほとんどの永続化レイヤに関するコードを既存のデータベーススキーマから生成してくれることがわかりました。

また、生成されたコードを分析し、NetBeansがすぐれたJPA機能(表を再生成するために使用できるアノテーション属性を含む)を持っていること、最大許容文字列長やフィールドのNULL可否、Beanバリデーションのサポートなどの情報を保持していることにも言及しました。

第2部では、再度NetBeansで利用できるJava EEツールを存分に活用してPet Clinicアプリケーションの書き換えを続けます。Data Access Object (DAO)として振る舞うEnterprise JavaBeans (EJB) 3.1のsession beanやJavaServer Faces (JSF) 2.0のmanaged beansやページを作成します。

[注]
この記事で説明しているサンプルのソースコードは以下のリンクよりダウンロードできます。
http://www.oracle.com/technetwork/articles/java/petclinicjavaee-1356466.zip

Session Bean、Managed Bean、JSFページの作成
JPAエンティティができたので、JSFページ、JSF Managed Bean、DAOを考えましょう。NetBeansのJSF Pages from Entity Classesウィザードを使えばすべて一揆に作成できます。このウィザードは[
ファイル] >[新規ファイル]>[持続性]から、[エンティティーからのJSFページクラス]を選択します(図1)
図1:エンティティーからのJSFページウィザード
既存のJPAエンティティを一つ以上選択する必要があります。[すべてを選択]ですべて選択することができます(図2)。
図2:JPAエンティティの選択
次に、生成されたSession BeanやJSF Managed Beanのパッケージを選択します(図3)。
図3:パッケージの選択
必要な情報を入力すると、NetBeansはJSFページを生成します。これらのページにはプロジェクトのJPAエンティティに対するすべてのCRUD操作(Create, Read, Update, Delete)のためのページが含まれています(図4)。
図4:NetBeansで生成したJSFページ
さらに、NetBeansはJSF Managed Beanの形で数多くのController(MVCでいうところの"C")、DAOとしてふるまうSession Bean、そしてユーティリティクラスを生成します(図5)。
図5:NetBeansが生成したController
ここで、JSF 2.0、EJB 3.1 Session Bean、JPA 2.0 Managed Beanを使ったJava EEアプリケーションの完成です。

生成したアプリケーションを動かしてみる
プロジェクトを右クリックしてRunを選択するだけで、アプリケーションを実行できます(図6)。
図6:アプリケーションの実行
デフォルトブラウザが立ち上がり、アプリケーションが動作していることを確認できます(図7)。
図7:実行中のアプリケーション
ご覧いただくとわかるとおり、生成されたindexページにはプロジェクトの全エンティティを管理するページへのリンクがあります。リンクをクリックすると、対応するデータベースのデータが表示されます(図8)。
図8:データベースのレコードを表示
NetBeansのウィザードが新しいアイテムを作成(Create)するためのリンクだけでなく、各アイテムの表示(Read)、編集(update)、削除(delete)のためのリンクを作成してくれていることがわかります。これらのリンクはアプリケーションのCRUD機能を提供しています。

任意のアイテムのViewリンクをクリックすると、対応するアイテムの全情報を見ることができます(図9)。
図9:アイテムの情報を表示
図9で、ペットの1件の情報を確認できます。主キーが表示されていることに注目してください。この情報はユーザに無関係なので削除すべきです。また、TypeやOwnerに対応する主キーもユーザーフレンドリーなテキスト表現はありません。生成コードを修正して、こうした変更を施すことは簡単にできます(この件については後ほど)。

viewページやlistページからEditリンクをクリックして、既存のデータベース中のレコードを変更できます(図10)。
図10:データベース中の既存のレコードを編集
生成されたページで少々変更しました。たとえば、生成されたJPAエンティティはJPA primary key generationを使っているので、主キーのためのフィールドを持つ必要はありません。TypeやOwnerのリストのオプションラベルは確かにユーザーフレンドリーではありません。こうした問題にも、少々の修正を生成されたコードに対して施せば、簡単に対応できます。(この件については後ほど)。

Create New Petのリンクをクリックすれば、新しいレコードをデータベースに追加することもできます(このUIはEditページにほとんど同じですので、ここではお見せしません)。Destroyリンクをクリックすれば、データベースからレコードを削除できます。

生成されたコードの精査
NetBeansのウィザードは多くのコード(JSFページ、JSF Managed Bean、ユーティリティクラス、EJB 3.1 Session Beanなど)を生成します。生成されたJSFページはきわめて単純で、標準的なJSFページなので、詳細には触れません。生成されたJSFページがFaceletsテンプレート機能を使うと、全面的に簡単にレイアウトを変更できることは注目すべきことでしょう。生成したテンプレートは十分に適切な名前、template.xhtmlという名前が付いています(コード1)。
コード1:生成されたFaceletsテンプレート
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title><ui:insert name="title">Default Title</ui:insert></title>
        <h:outputStylesheet name="css/jsfcrud.css"/>
    </h:head>
    <h:body>
        <h1>
             <ui:insert name="title">Default Title</ui:insert>
        </h1>
        <p>
             <ui:insert name="body">Default Body</ui:insert>
        </p>
     </h:body>
</html>
驚くことではありませんが、生成されたテンプレートがFaceletsの<ui:insert>タグを使って、テンプレートで全てのページの変更対象のエリアをマークしています。生成されたほとんどのページには、対応する<ui:define>タグがあることが確認できます(コード2)。
コード2:“View Pet” JSFページ
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">

  <ui:composition template="/template.xhtml">
        <ui:define name="title">
             <h:outputText value="#{bundle.ViewPetTitle}"></h:outputText>
        </ui:define>
        <ui:define name="body">
             <!-- Content omitted for brevity -->
        </ui:define>
  </ui:composition>
</html>
JSFページは自動生成されたCSSスタイルシートでスタイルが指定されていることにも言及しておきましょう。生成されたJSFページは、JSF 2.0で導入された、標準リソースディレクトリの機能を使用しています。CSSスタイルシートはFaceletsテンプレート中の以下のマークアップに含まれています。
<h:outputStylesheet name="css/jsfcrud.css"/>
デフォルトで、JSF実装はresourcesという名前のディレクトリを探します。これはMETA-INFの中もしくはWARファイルのルートにあります。resourcesディレクトリ中にcssというサブディレクトリを探し、特定のCSSスタイルシートをcssディレクトリで探します。ディレクトリ構造は対応するディレクトリをNetBeansプロジェクトビューで拡大すれば確認できます(図11)。
図11:ディレクトリ構造の表示
JSFページに加え、ウィザードは数多くのJSF Managed Beanを作成します。これらはアプリケーションにてcontrollerとして振る舞います(図12)。
図12:生成されたJSF Managed Bean
Controllerのほとんどのメソッドは基本的にパススルーするメソッドであり、Session BeanのEntityManagerファサードの対応するメソッドを呼び出しますが、いくつかのメソッドの内部を見てみましょう(コード3)。
コード3:Controllerメソッド
package com.ensode.petclinicjavaee.managedbean;
import com.ensode.petclinicjavaee.entity.Pet;
/* Imports Omitted */
@ManagedBean(name = "petController")
@SessionScoped
public class PetController implements Serializable {
    private Pet current;
    private DataModel items = null;
    @EJB
    private com.ensode.petclinicjavaee.session.PetFacade ejbFacade;
    private PaginationHelper pagination;
    private int selectedItemIndex;
    public PetController() {
    }

   /* getSelected() method omitted */
   /* getFacade() method omitted */
    public PaginationHelper getPagination() {
        if (pagination == null) {
            pagination = new PaginationHelper(10) {
                @Override
                public int getItemsCount() {
                    return getFacade().count();
                }
                @Override
                public DataModel createPageDataModel() {
                    return new ListDataModel(getFacade().findRange(new int[]{
                                getPageFirstItem(), getPageFirstItem() + getPageSize()}));
                }
            };
        }
        return pagination;
    }

    /* prepareList() method omitted */
    public String prepareView() {
        current = (Pet) getItems().getRowData();
        selectedItemIndex = pagination.getPageFirstItem() + getItems().
                getRowIndex();
        return "View";
    }

    public String prepareCreate() {
        current = new Pet();
        selectedItemIndex = -1;
        return "Create";
    }

    /* create method omitted */
    public String prepareEdit() {
        current = (Pet) getItems().getRowData();
        selectedItemIndex = pagination.getPageFirstItem() + getItems().
                getRowIndex();
        return "Edit";
    }

    /* update method omitted */
    /* destroy method omitted */
    /* destroyAndView() method omitted */
    /* performDestroy() method omitted */
    /* updateCurrentItem() method omitted */
    public DataModel getItems() {
        if (items == null) {
            items = getPagination().createPageDataModel();
        }
        return items;
    }

    private void recreateModel() {
        items = null;
    }

    public String next() {
        getPagination().nextPage();
        recreateModel();
        return "List";
    }

    public String previous() {
        getPagination().previousPage();
        recreateModel();
        return "List";
    }

    public SelectItem[] getItemsAvailableSelectMany() {
        return JsfUtil.getSelectItems(ejbFacade.findAll(), false);
    }

    public SelectItem[] getItemsAvailableSelectOne() {
        return JsfUtil.getSelectItems(ejbFacade.findAll(), true);
    }

    @FacesConverter(forClass = Pet.class)
    public static class PetControllerConverter implements Converter {

        public Object getAsObject(FacesContext facesContext,
                UIComponent component, String value) {
            if (value == null || value.length() == 0) {
                return null;
            }
            PetController controller = (PetController) facesContext.
                    getApplication().getELResolver().
                    getValue(facesContext.getELContext(), null, "petController");
            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 Pet) {
                Pet o = (Pet) object;
                return getStringKey(o.getId());
            } else {
                throw new IllegalArgumentException("object " + object +
                        " is of type " + object.getClass().getName() +
                        "; expected type: " + PetController.class.getName());
            }
        }
    }
}
JSF Managed Beanは@FacesConverterアノテーションが付いた内部クラスを持っています。このアノテーションはJSF 2.0で導入され、注釈が付いたクラスはJSFコンバータとして振る舞います。
JSFコンバータはJavaオブジェクトを文字列に変換(その逆もあり)します。getAsObject()メソッドは文字列を受け取り対応するオブジェクトに変換します。生成されたこのメソッドの実装は、JPAエンティティのidプロパティの文字列を取り、それを対応する整数型に変換し、生成されたSessionファサードでデータベースをクエリして、クエリ結果に対応するJPAエンティティオブジェクトを取得します。生成されたgetAsString() メソッドは単にJPAエンティティのIdプロパティの値を文字列として返すだけです。

注目すべきことがまだあります。生成されたJSF Managed BeanはJSF DataModel実装を使います(コード4)。JSF DataModelインターフェースを使うと、JSFデータテーブルに表示されるデータの操作が簡単になります。生成されたJSF Managed BeanもまたPaginationHelperというユーティリティクラスを使います。このクラスは、改ページ位置の自動修正機能を提供します。つまり、リストの残りの部分に"前"と"次"のリンクを提供しながら、ページ上で同時にいくつかの項目を表示する機能です。
コード4:PaginationHelper.java
package com.ensode.petclinicjavaee.managedbean.util;

import javax.faces.model.DataModel;

public abstract class PaginationHelper {
    private int pageSize;
    private int page;

    public PaginationHelper(int pageSize) {
        this.pageSize = pageSize;
    }

    public abstract int getItemsCount();

    public abstract DataModel createPageDataModel();

    public int getPageFirstItem() {
        return page * pageSize;
    }

    public int getPageLastItem() {
        int i = getPageFirstItem() + pageSize - 1;
        int count = getItemsCount() - 1;
        if (i > count) {
            i = count;
        }
        if (i < 0) {
            i = 0;
        }
        return i;
    }

    public boolean isHasNextPage() {
        return (page + 1) * pageSize + 1 <= getItemsCount();
    }

    public void nextPage() {
        if (isHasNextPage()) {
            page++;
        }
    }

    public boolean isHasPreviousPage() {
        return page > 0;
    }

    public void previousPage() {
        if (isHasPreviousPage()) {
            page--;
        }
    }

    public int getPageSize() {
        return pageSize;
    }
}
おわかりのように、PaginationHelperは抽象クラスで、2個の抽象メソッドを提供しています。一つはgetItemsCount()で、もう一つはcreatePageDataModel() です。各々の生成されたJSF Managed Beanは、PaginationHelperクラスを拡張している匿名内部クラスを提供しています。この匿名クラスでは上記2メソッドの具体的な実装を提供しています(図5)。
コード5:PaginationHelperクラスを拡張した匿名内部クラス
    public PaginationHelper getPagination() {
        if (pagination == null) {
            pagination = new PaginationHelper(10) {

                @Override
                public int getItemsCount() {
                    return getFacade().count();
                }

                @Override
                public DataModel createPageDataModel() {
                    return new ListDataModel(getFacade().findRange(new int[]{
                                getPageFirstItem(), getPageFirstItem() + getPageSize()}));
                }
            };
        }
        return pagination;
    }
getItemsCount() の実装は、生成されたEJBファサードのcount()メソッドを呼び出すだけです。これは、JPAエンティティにマッピングされているデータベース表の行数を順番に返します。

createPageDataModel() メソッドはもうちょっと興味深いものです。LazyローディングStrategyを実装しているのです。つまり、ページに表示されているアイテムのみデータベースからリアルタイムに取り出す、というものです。通常はデータベースから全件取り出し、別のアイテムで改ページするためにメモリ上に種々のアイテムを保持しておかねばならないため、これはメモリの節約に役立ちます。
生成されたJSFコードをみてきたので、生成されたEJB 3.1 Session Beanを見ていきましょう。全ての生成されたSession BeanはAbstractFacade.java(コード6)と呼ばれる親クラスを拡張したものです。
コード6:AbstractFacade.java
package com.ensode.petclinicjavaee.session;

/* imports omitted */

public abstract class AbstractFacade {

    private Class entityClass;

    public AbstractFacade(Class entityClass) {
        this.entityClass = entityClass;
    }

    protected abstract EntityManager getEntityManager();

    public void create(T entity) {
        getEntityManager().persist(entity);
    }

    public void edit(T entity) {
        getEntityManager().merge(entity);
    }

    public void remove(T entity) {
        getEntityManager().remove(getEntityManager().merge(entity));
    }

    public T find(Object id) {
        return getEntityManager().find(entityClass, id);
    }

    public List findAll() {
        javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
        cq.select(cq.from(entityClass));
        return getEntityManager().createQuery(cq).getResultList();
    }

    public List findRange(int[] range) {
        javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
        cq.select(cq.from(entityClass));
        javax.persistence.Query q = getEntityManager().createQuery(cq);
        q.setMaxResults(range[1] - range[0]);
        q.setFirstResult(range[0]);
        return q.getResultList();
    }

    public int count() {
        javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
        javax.persistence.criteria.Root rt = cq.from(entityClass);
        cq.select(getEntityManager().getCriteriaBuilder().count(rt));
        javax.persistence.Query q = getEntityManager().createQuery(cq);
        return ((Long) q.getSingleResult()).intValue();
    }
}
AbstractFacadeはFacadeデザインパターンを実装しています。つまり、JPA EntityManagerへのシンプルなインターフェースを提供する、というものです。生成されたメソッドはJPA 2.0 criteria APIを使います。このAPIはJPA仕様への主要な追加項目であり、動的にタイプセーフなJPAクエリを作成することができるというものです。

生成されたSession Beanによって行われる作業のほとんどは、親クラスが実施します。子クラスがやることは、コンストラクタを実装することに加え、EntityManagerのインスタンスをJava EE Dependency Injectionメカニズムを使って注入することです(コード7)。
コード7:EntityManagerの注入
package com.ensode.petclinicjavaee.session;

/* imports omitted */

@Stateless
public class PetFacade extends AbstractFacade {

    @PersistenceContext(unitName = "PetClinicJavaEEPU")
    private EntityManager em;

    protected EntityManager getEntityManager() {
        return em;
    }

    public PetFacade() {
        super(Pet.class);
    }
}
おわかりのように、子クラスのコンストラクタは単に親クラスのコンストラクタを呼び出し、このSession Beanが操作するJPAエンティティの型を渡しているだけです。親クラスは順にgenericsを使って動的に正しい型をentityClassインスタンスの値に追加します。
@PersistenceContextアノテーションで注釈付けし、(persistence.xmlで定義されている)Persistence Unitの名前を渡すと、EntityManagerのインスタンスを注入することができます。ここは特に何もありません。

まとめ
この投稿では、生成されたアプリケーションが動作していること、「ボンネットの下」を見て何が起きているのかを確認しました。第3部では、生成されたコードを変更してユーザフレンドリーにし、Pet ClinicアプリケーションのJava EE版とSpring版を比較してみようと思います。

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

0 件のコメント:

コメントを投稿