分速で始める JavaEE 7 〜 JPA + CDI + JSF 〜


f:id:Naotsugu:20150213230909p:plain

前回の続き 秒速で始める Java EE 7 with Wildfly - A Memorandum

せっかくなので wildfly の quickstart をベースに、分速で簡単なアプリにしてみます。

設定ファイル準備

最初に src 以下にディレクトリを掘っておきます。

mkdir -p src/main/java/example/controller
mkdir -p src/main/java/example/data
mkdir -p src/main/java/example/model
mkdir -p src/main/java/example/service
mkdir -p src/main/resources/META-INF
mkdir -p src/main/webapp/resources/css
mkdir -p src/main/webapp/WEB-INF/templates

こんな感じになります。

f:id:Naotsugu:20150214162314p:plain

永続化設定

wildfly に H2 入っているので、そのままデータソースの定義ファイルを作成します。

example-ds.xml
touch src/main/webapp/WEB-INF/example-ds.xml

以下のように定義しておくと、wildfly がデータソースを作成してくれます。

<?xml version="1.0" encoding="UTF-8"?>
<datasources xmlns="http://www.jboss.org/ironjacamar/schema"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://www.jboss.org/ironjacamar/schema http://docs.jboss.org/ironjacamar/schema/datasources_1_0.xsd">
    <datasource jndi-name="java:jboss/datasources/exampleDS"
                pool-name="example-pool" enabled="true" use-java-context="true">
        <connection-url>jdbc:h2:mem:example;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1</connection-url>
        <driver>h2</driver>
        <security>
            <user-name>sa</user-name>
            <password>sa</password>
        </security>
    </datasource>
</datasources>

DBは h2 のインメモリDBにするので、jdbc:h2:mem とします。データソースの名前は jboss/datasources/exampleDS としました。            

persistence.xml

データソースをJPAで使うので persistence.xml を用意します。

touch src/main/resources/META-INF/persistence.xml

中身は以下。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
    <persistence-unit name="primary">
        <jta-data-source>java:jboss/datasources/exampleDS</jta-data-source>
        <properties>
            <property name="hibernate.hbm2ddl.auto" value="create-drop" />
            <property name="hibernate.show_sql" value="true" />
        </properties>
    </persistence-unit>
</persistence>

データソース名に先ほど定義した jboss/datasources/exampleDS を指定します。 永続化プロバイダ向けのプロパティとして hibernate.hbm2ddl.auto を指定し、テーブルを自動生成するようにします。

CDI 設定

CDI の設定ファイル作成しておきます。

touch src/main/webapp/WEB-INF/beans.xml

CDI1.1 用の bean.xml です。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd" bean-discovery-mode="all">
</beans>

bean-descovery-mode には all を指定しています。これによりアプリケーションに含まれる全ての Bean が CDI によるインジェクションの対象となります。通常は annotated を指定し、CDI のスコープアノテーションが付いたもののみをインジェクション対象とすることが推奨されていますが。

CDI1.1 からは bean.xml ファイルが無い場合には bean-descovery-modeannotated を指定したものとして動作します。

JSF 設定

faces-config.xml を作成します。

touch src/main/webapp/WEB-INF/faces-config.xml

JSF 2.2 を有効にするためのみに利用するので中身は空です。

<?xml version="1.0"?>
<faces-config version="2.2"
              xmlns="http://xmlns.jcp.org/xml/ns/javaee"
              xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xsi:schemaLocation="
        http://xmlns.jcp.org/xml/ns/javaee
        http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
</faces-config>

クラスファイル準備

さて、設定ファイルは揃いました。 コードに移るので、クラスファイルだけ事前に作成しておきましょう。

touch src/main/java/example/controller/MemberController.java
touch src/main/java/example/data/MemberListProducer.java
touch src/main/java/example/data/MemberRepository.java
touch src/main/java/example/model/Member.java
touch src/main/java/example/service/MemberRegistration.java
touch src/main/java/example/Resources.java

Resources.java

最初に簡単なユーティリティーを作成しておきます。

package example;

import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Produces;
import javax.faces.context.FacesContext;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

public class Resources {

    @Produces
    @PersistenceContext
    private EntityManager em;

    @Produces
    @RequestScoped
    public FacesContext produceFacesContext() {
        return FacesContext.getCurrentInstance();
    }

}

@Produces は、CDI のインジェクションポイントに対して、インジェクション対象を供給するためのマークです。 この定義により、@Inject でマークされた EntityManager や FacesContext の型に対して@Produces 経由でインジェクトされます。

DI のインスタンスを制御でき、Qualifier アノテーションで切り替えができたり非常に強力ですが、乱用すると見通しが悪くなるので利用には注意が必要です。

Member.java

Member エンティティです。@Entity を付与します。 name と email フィールドと id フィールドを定義します。

package example.model;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlRootElement;

@SuppressWarnings("serial")
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "email"))
public class Member implements Serializable {
    
    @Id
    @GeneratedValue
    private Long id;

    @NotNull
    @Size(min = 1, max = 25)
    @Pattern(regexp = "[^0-9]*", message = "Must not contain numbers")
    private String name;

    @NotNull
    @Pattern(regexp = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$", message = "not email")
    private String email;

   public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

}

単純な JPA の Entity 定義です。フィールドは Bean Validation のアノテーション付けて入力チェックします。 テーブルには @UniqueConstraint にて email フィールドにユニーク制約を付けています。

メールアドレスのチェックは Hibernate Validator の @Email 使うべきですが、ライブラリへの依存を最小限にしたかったので。

MemberRepository.java

リポジトリです。Member を id で取得するメソッドと、email で取得するメソッド。一覧を取得するメソッドを定義します。

package example.data;

import example.model.Member;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
import java.util.List;

@ApplicationScoped
public class MemberRepository {

    @Inject
    private EntityManager em;

    public Member findById(Long id) {
        return em.find(Member.class, id);
    }

    public Member findByEmail(String email) {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Member> criteria = cb.createQuery(Member.class);
        Root<Member> member = criteria.from(Member.class);
        criteria.select(member).where(cb.equal(member.get("email"), email));
        return em.createQuery(criteria).getSingleResult();
    }

    public List<Member> findAllOrderedByName() {
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<Member> criteria = cb.createQuery(Member.class);
        Root<Member> member = criteria.from(Member.class);
        criteria.select(member).orderBy(cb.asc(member.get("name")));
        return em.createQuery(criteria).getResultList();
    }
}

EntityManager は先ほど @Produces で定義しておいたので、@Inject で DI されます。 各メソッドでは単純に CriteriaQuery でクエリしているだけです。

ここではクエリだけを扱い、コマンド(状態を変化させるプロシージャで、insert などの処理)は扱っていないことに注意してください。コマンドは別クラスに局在させます。

スコープアノテーションとして @ApplicationScoped を指定しています。 EntityManager は仕様上スレッドセーフの保証はされていませんが、Java EE コンテナ管理のトランザクション配下で動く場合には、永続化プロバイダの提供する EntityManager をラップするクラスが間に入ります。 wildfly を利用した上記例だと CDI 経由しているので、weld の ProxyFactory で作成されたプロキシが CDI のスコープを管理しつつ、実際の処理は org.jboss.as.jpa.container.TransactionScopedEntityManager に委譲され、このクラスがトランザクション別に EntityManagerFactory.createEntityManager() しているので、ApplicationScoped としつつも EntityManager はトランザクション毎で別のインスタンスが利用されます。この辺は色々な仕様がからまってかなり混乱するので深追いしません。コンテナの実装にもよるので全て @RequestScoped にするか @Stateless で統一しちゃった方が安全かもしれません。

MemberRegistration.java

Member の登録用のステートレスセッションビーンです。

package example.service;

import example.model.Member;

import javax.ejb.Stateless;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.persistence.EntityManager;

@Stateless
public class MemberRegistration {

    @Inject
    private EntityManager em;

    @Inject
    private Event<Member> memberEventSrc;

    public void register(Member member) throws Exception {
        em.persist(member);
        memberEventSrc.fire(member);
    }
}

登録後のイベント通知です。Member の新規登録を CDI の Event で通知しています。 この通知を受けるのは次の MemberListProducer です。 ただ、Event は乱用すると見通しが悪くなるだけなので利用は慎重にすべきです。

MemberListProducer.java

Member の一覧を供給する CDIネーブルドビーンです。

package example.data;

import example.model.Member;

import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.event.Observes;
import javax.enterprise.event.Reception;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.List;

@RequestScoped
public class MemberListProducer {

    @Inject
    private MemberRepository memberRepository;

    private List<Member> members;

    @Produces
    @Named
    public List<Member> getMembers() {
        return members;
    }

    public void onMemberListChanged(@Observes(notifyObserver = Reception.IF_EXISTS) final Member member) {
        retrieveAllMembersOrderedByName();
    }

    @PostConstruct
    public void retrieveAllMembersOrderedByName() {
        members = memberRepository.findAllOrderedByName();
    }
}

@RequestScoped でリクエスト毎にインスタンスが作成されます。 @PostConstruct にてインスタンス作成のタイミングで、内部に持つ Member のリストを更新します。

先ほどの MemberRegistration から Member の登録イベントが発行されると、@Observes により Member の更新が通知されます。 getMembers@Named@Produces なので、EL式などから名前で参照可能となります。

MemberController.java

JSF のバッキングビーンになります。

package example.controller;

import example.model.Member;
import example.service.MemberRegistration;

import javax.annotation.PostConstruct;
import javax.enterprise.inject.Model;
import javax.enterprise.inject.Produces;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;

@Model
public class MemberController {

    @Inject
    private FacesContext facesContext;

    @Inject
    private MemberRegistration memberRegistration;

    @Produces
    @Named
    private Member newMember;

    @PostConstruct
    public void initNewMember() {
        newMember = new Member();
    }

    public void register() throws Exception {
        try {
            memberRegistration.register(newMember);
            FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_INFO, "Registered!", "Registration successful");
            facesContext.addMessage(null, m);
            initNewMember();
            
        } catch (Exception e) {
            String errorMessage = "Registration failed. See server log for more information";
            FacesMessage m = new FacesMessage(FacesMessage.SEVERITY_ERROR, errorMessage, "Registration unsuccessful");
            facesContext.addMessage(null, m);
        }
    }
}

xhtml 側から newMember で参照できるように @Named@Produces なフィールドを作成します。 あとは登録メソッドですね。

ビューテンプレート

最後に xhtmlcss 作成します。

touch src/main/webapp/resources/css/screen.css
touch src/main/webapp/WEB-INF/templates/default.xhtml
touch src/main/webapp/index.xhtml
default.xhtml

テンプレートです。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:ui="http://java.sun.com/jsf/facelets">
<h:head>
    <title>Sample</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <link href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css" rel="stylesheet"/>
    <h:outputStylesheet name="css/screen.css" />
</h:head>

<h:body>
    <div id="container">
        <div id="content">
            <ui:insert name="content">
                [Template content will be inserted here]
            </ui:insert>
        </div>
        <div id="footer">
            <p>Sample of JavaEE 7.<br /></p>
        </div>
    </div>
</h:body>
</html>

bootstrap の CSS は CDN で取ります。

index.xhtml

Member の登録と一覧画面です。

<?xml version="1.0" encoding="UTF-8"?>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:h="http://java.sun.com/jsf/html"
                template="/WEB-INF/templates/default.xhtml">
    <ui:define name="content">

        <h:form id="reg">
            <h2>Member Registration</h2>
            <h:panelGrid columns="3" columnClasses="titleCell">
                <h:outputLabel for="name" value="Name:" />
                <h:inputText id="name" value="#{newMember.name}" />
                <h:message for="name" errorClass="invalid" />

                <h:outputLabel for="email" value="Email:" />
                <h:inputText id="email" value="#{newMember.email}" />
                <h:message for="email" errorClass="invalid" />
            </h:panelGrid>
            <p>
                <h:panelGrid columns="2">
                    <h:commandButton id="register"
                                     action="#{memberController.register}"
                                     value="Register" styleClass="btn btn-primary" />
                    <h:messages styleClass="messages"
                                errorClass="invalid" infoClass="valid"
                                warnClass="warning" globalOnly="true" />
                </h:panelGrid>
            </p>
        </h:form>
        
        <hr/>
        <h2>Members</h2>
        <h:panelGroup rendered="#{empty members}">
            <em>No registered members.</em>
        </h:panelGroup>
        <h:dataTable var="_member" value="#{members}" rendered="#{not empty members}" 
                     styleClass="table table-striped table-bordered">
            <h:column>
                <f:facet name="header">Id</f:facet>
                #{_member.id}
            </h:column>
            <h:column>
                <f:facet name="header">Name</f:facet>
                #{_member.name}
            </h:column>
            <h:column>
                <f:facet name="header">Email</f:facet>
                #{_member.email}
            </h:column>
        </h:dataTable>
    </ui:define>
</ui:composition>

テンプレートに先ほど作成した default.xhtml を指定します。

#{newMember}MemberControllernewMember フィールドの内容になります。

#{members}MemberListProducergetMembers() が呼ばれます。

いずれも @Named なのでこのように参照できます。

登録操作は#{memberController.register} にあるように MemberControllerregister() が呼ばれすことになります。 memberController という名前で参照できるのは @Model@Named でアノテートされているためです。

screen.css

最後に簡単なCSS

#container {
  padding: 20px
}

以下のようになります。

f:id:Naotsugu:20150215220947p:plain

実行

-i オプションでログ出しながら起動してみましょう。

./gradlew war cargoRunLocal -i

起動したら以下をブラウザで開きます。

http://localhost:8080/example/index.jsf

f:id:Naotsugu:20150215214202p:plain

登録してみます。

f:id:Naotsugu:20150215214209p:plain

登録されました。

f:id:Naotsugu:20150215214216p:plain

ちょっとファイル数多くなり、さすがに秒速ってわけにはいきませんが、数分でいけるのではないでしょうか。 せっかくなので、次回 分速で始める JavaEE 7 〜 JPA + CDI + JAX-RS 〜 - A Memorandum までやって終わります。