前回の続き
せっかくなので 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
こんな感じになります。
永続化設定
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-mode
に annotated
を指定したものとして動作します。
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
なフィールドを作成します。
あとは登録メソッドですね。
ビューテンプレート
最後に xhtml と css 作成します。
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}
は MemberController
の newMember
フィールドの内容になります。
#{members}
で MemberListProducer
の getMembers()
が呼ばれます。
いずれも @Named
なのでこのように参照できます。
登録操作は#{memberController.register}
にあるように MemberController
の register()
が呼ばれすことになります。
memberController
という名前で参照できるのは @Model
が @Named
でアノテートされているためです。
screen.css
最後に簡単なCSS
#container { padding: 20px }
以下のようになります。
実行
-i
オプションでログ出しながら起動してみましょう。
./gradlew war cargoRunLocal -i
起動したら以下をブラウザで開きます。
http://localhost:8080/example/index.jsf
登録してみます。
登録されました。
ちょっとファイル数多くなり、さすがに秒速ってわけにはいきませんが、数分でいけるのではないでしょうか。 せっかくなので、次回
までやって終わります。