Chapter 11. Seam应用程序框架

Seam通过编写带有注解的简单Java类来让创建应用程序的工作变得非常简单,不需扩展任何特定接口和父类。但常见的编程任务还能进一步简化,这是通过一组预先创建的组件进行的,它们能够由 component.xml 文件配置(最简单的情况)或者类扩展而实现复用。

在一个Web应用程序中使用Hibernate或者JPA进行基本的数据库操作时,Seam Application Framework(Seam应用程序框架) 能够减少你需要书写的代码量。

我们需要强调的是,这个框架非常的简单,只是少量的易于理解和扩展的简单类。 “魔力”来自于Seam自身 — 即使没有用这个框架来创建任何Seam应用程序的时候,你也同样用到这一“魔力”。

11.1. 简介

有两种不同的方法使用Seam Application Framework所提供的组件。第一种方法是像处理其他种类的Seam内置组件一样,在 components.xml 中安装和配置组件的实例。 举例来说,下列 components.xml 中的片段安装了一个能够为 Person 实体执行基本的CRUD(创建(Create)、读取(Retrieve) 、更新(Update)和删除(Delete))操作的组件:

<framework:entity-home name="personHome"
                       entity-class="eg.Person"
                       entity-manager="#{personDatabase}">
    <framework:id>#{param.personId}</framework:id>
</framework:entity-home>

如果上面的代码按你的口味来说太像“用XML编程”,你可以改为使用扩展:

@Stateful
@Name("personHome")
public class PersonHome extends EntityHome<Person> implements LocalPersonHome {
    @RequestParameter String personId;
    @In EntityManager personDatabase;

    public Object getId() { return personId; }
    public EntityManager getEntityManager() { return personDatabase; }

}

第二种方法有一个很大的优点:你能够方便地添加额外的功能,覆盖内置的功能(框架的类都精心设计以便于扩展和定制)。

第二个优点是:如果你喜欢的话,你的类可以是有状态会话Bean(这不是必须的,也可以是普通的JavaBean组件,如果你喜欢的话)。如果你正在使用JBoss AS,你需要使用4.2.2.GA或更高的版本。

目前,Seam应用框架提供了四个内置的组件:用于CRUD操作的 EntityHomeHibernateEntityHome 以及用于查询的 EntityQueryHibernateEntityQuery

你得编写Home和Query组件,它们能在session、event或conversation作用范围中运行,至于选择哪个scope取决于你所希望在你的应用程序中使用的状态模型。

Seam应用框架仅在Seam管理的持久化上下文中工作。默认情况下,这些组件会寻找一个叫做 entityManager 的持久化上下文。

11.2. Home对象

Home对象对特定的实体类提供持久化操作,假设我们有个可靠的 Person 类:

@Entity
public class Person {
    @Id private Long id;
    private String firstName;
    private String lastName;
    private Country nationality;

    //getters and setters...
}

我们可以通过配置定义一个 personHome 组件:

<framework:entity-home name="personHome" entity-class="eg.Person" />

也可以通过类的扩展

@Name("personHome")
public class PersonHome extends EntityHome<Person> {}

Home对象提供了如下的操作:persist()remove()update()getInstance()。 在你能够调用 remove()update() 操作之前,你必须首先使用 setId() 方法定义你感兴趣的对象的标识符。

我们可以直接从一个JSF页面使用一个Home,如下例:

<h1>Create Person</h1>
<h:form>
    <div>First name: <h:inputText value="#{personHome.instance.firstName}"/></div>
    <div>Last name: <h:inputText value="#{personHome.instance.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}"/>
    </div>
</h:form>

通常,只用person 指明person漂亮得多,所以在 components.xml 中添加一行语句来实现。

<factory name="person"
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome"
                       entity-class="eg.Person" />

(如果我们使用配置的方法。) 或者,我们可以通过向 PersonHome 中添加一个 @Factory 方法来实现:

@Name("personHome")
public class PersonHome extends EntityHome<Person> {

    @Factory("person")
    public Person initPerson() { return getInstance(); }

}

(如果我们使用类扩展的方法) 这个修改使我们的JSF页面简化如下:

<h1>Create Person</h1>
<h:form>
    <div>First name: <h:inputText value="#{person.firstName}"/></div>
    <div>Last name: <h:inputText value="#{person.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}"/>
    </div>
</h:form>

好,这就可以用来创建新的 Person 实体了。是的,这就是所需的全部代码!现在,如果我们想显示,更新,删除数据库中已经存在的 Person 实体,我们需要将实体标识符传递给 PersonHome。页面参数是一种非常好的实现方式:

<pages>
    <page view-id="/editPerson.jsp">
        <param name="personId" value="#{personHome.id}"/>
    </page>
</pages>

现在,我们可以向JSF页面中增加其他的操作:

<h1>
    <h:outputText rendered="#{!personHome.managed}" value="Create Person"/>
    <h:outputText rendered="#{personHome.managed}" value="Edit Person"/>
</h1>
<h:form>
    <div>First name: <h:inputText value="#{person.firstName}"/></div>
    <div>Last name: <h:inputText value="#{person.lastName}"/></div>
    <div>
        <h:commandButton value="Create Person" action="#{personHome.persist}" rendered="#{!personHome.managed}"/>
        <h:commandButton value="Update Person" action="#{personHome.update}" rendered="#{personHome.managed}"/>
        <h:commandButton value="Delete Person" action="#{personHome.remove}" rendered="#{personHome.managed}"/>
    </div>
</h:form>

当我们没有带任何请求参数链接到该页面时,会显示"Create Person"页面,当我们为 personId 这个请求参数设定一个值时,会显示“Edit Person”页面。

假设我们需要创建一些 Person 实体,并且初始化这些人的国籍。我们可以通过配置很轻松地完成:

<factory name="person"
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome"
                       entity-class="eg.Person"
                       new-instance="#{newPerson}"/>

<component name="newPerson"
           class="eg.Person">
    <property name="nationality">#{country}</property>
</component>

也可以通过扩展类

@Name("personHome")
public class PersonHome extends EntityHome<Person> {

    @In Country country;

    @Factory("person")
    public Person initPerson() { return getInstance(); }

    protected Person createInstance() {
        return new Person(country);
    }

}

当然,Country 是一个被其它的Home对象管理的对象,比如说,CountryHome

为了增加更多复杂的操作(联合管理等等),我们可以向 PersonHome 中添加方法。

@Name("personHome")
public class PersonHome extends EntityHome<Person> {

    @In Country country;

    @Factory("person")
    public Person initPerson() { return getInstance(); }

    protected Person createInstance() {
        return new Person(country);
    }

    public void migrate()
    {
        getInstance().setCountry(country);
        update();
    }

}

当事务成功之后(调用 persist()update()remove() 成功后),Home对象会发出一个 org.jboss.seam.afterTransactionSuccess 事件。 通过监听这一事件,我们可以在底层实体改变后,刷新查询。 如果我们只需要在特定的实体保存、修改或删除后刷新特定查询,我们可以监视 org.jboss.seam.afterTransactionSuccess.<name> 事件(<name> 是实体的名字)。

当一个操作成功时,Home对象可以自动地显示Faces信息,我们可以再一次通过配置来定制信息。

<factory name="person"
         value="#{personHome.instance}"/>

<framework:entity-home name="personHome"
                       entity-class="eg.Person"
                       new-instance="#{newPerson}">
    <framework:created-message>New person #{person.firstName} #{person.lastName} created</framework:created-message>
    <framework:deleted-message>Person #{person.firstName} #{person.lastName} deleted</framework:deleted-message>
    <framework:updated-message>Person #{person.firstName} #{person.lastName} updated</framework:updated-message>
</framework:entity-home>

<component name="newPerson"
           class="eg.Person">
    <property name="nationality">#{country}</property>
</component>

或者扩展:

@Name("personHome")
public class PersonHome extends EntityHome<Person> {

    @In Country country;

    @Factory("person")
    public Person initPerson() { return getInstance(); }

    protected Person createInstance() {
        return new Person(country);
    }

    protected String getCreatedMessage() { return "New person #{person.firstName} #{person.lastName} created"; }
    protected String getUpdatedMessage() { return "Person #{person.firstName} #{person.lastName} updated"; }
    protected String getDeletedMessage() { return "Person #{person.firstName} #{person.lastName} deleted"; }

}

但是指定信息最好的方法是把信息置于Seam所知的resource bundle中(在默认情况下,这个bundle叫做 messages )。

Person_created=New person #{person.firstName} #{person.lastName} created
Person_deleted=Person #{person.firstName} #{person.lastName} deleted
Person_updated=Person #{person.firstName} #{person.lastName} updated

这样方便进行国际化,从表现层的角度考虑也保持了代码和配置的整洁。

最后一步是使用 <s:validateAll><s:decorate> 向页面中添加验证功能,我会把这个留给你们自己去实现。

11.3. Query对象

如果我们需要数据库中所有 Person 实例的列表,我们可以使用Query对象,例如:

<framework:entity-query name="people"
                        ejbql="select p from Person p"/>

我们可以从一个JSF页面中使用它:

<h1>List of people</h1>
<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

我们可能需要支持分页:

<framework:entity-query name="people"
                        ejbql="select p from Person p"
                        order="lastName"
                        max-results="20"/>

我们可以使用page参数来决定被显示的页面

<pages>
    <page view-id="/searchPerson.jsp">
        <param name="firstResult" value="#{people.firstResult}"/>
    </page>
</pages>

用于分页的JSF代码可能有点冗长,但仍然是便于管理的:

<h1>Search for people</h1>
<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="First Page">
    <f:param name="firstResult" value="0"/>
</s:link>

<s:link view="/search.xhtml" rendered="#{people.previousExists}" value="Previous Page">
    <f:param name="firstResult" value="#{people.previousFirstResult}"/>
</s:link>

<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Next Page">
    <f:param name="firstResult" value="#{people.nextFirstResult}"/>
</s:link>

<s:link view="/search.xhtml" rendered="#{people.nextExists}" value="Last Page">
    <f:param name="firstResult" value="#{people.lastFirstResult}"/>
</s:link>

真实的搜索界面能够通过让用户输入一系列的可选的搜索标准来缩小返回的结果列表。Query对象通过让你指定可选的“约束”来支持这个重要的用例。

<component name="examplePerson" class="Person"/>

<framework:entity-query name="people"
                        ejbql="select p from Person p"
                        order="lastName"
                        max-results="20">
    <framework:restrictions>
        <value>lower(firstName) like lower( concat(#{examplePerson.firstName},'%') )</value>
        <value>lower(lastName) like lower( concat(#{examplePerson.lastName},'%') )</value>
    </framework:restrictions>
</framework:entity-query>

注意“example”对象的使用。

<h1>Search for people</h1>
<h:form>
    <div>First name: <h:inputText value="#{examplePerson.firstName}"/></div>
    <div>Last name: <h:inputText value="#{examplePerson.lastName}"/></div>
    <div><h:commandButton value="Search" action="/search.jsp"/></div>
</h:form>

<h:dataTable value="#{people.resultList}" var="person">
    <h:column>
        <s:link view="/editPerson.jsp" value="#{person.firstName} #{person.lastName}">
            <f:param name="personId" value="#{person.id}"/>
        </s:link>
    </h:column>
</h:dataTable>

在底层实体发生改变后,可以通过监听 org.jboss.seam.afterTransactionSuccess 事件来刷新查询:

<event type="org.jboss.seam.afterTransactionSuccess">
    <action execute="#{people.refresh}" />
</event>

或者,在发生持久化、更新或者删除时,通过 PersonHome 来刷新查询:

<event type="org.jboss.seam.afterTransactionSuccess.Person">
    <action execute="#{people.refresh}" />
</event>

这个部分所有的例子都是通过配置来体现重用的,但是,对Query对象通过扩展来进行重用也是可行的。

11.4. Controller对象

Controller 类以及它的子类 EntityControllerHibernateEntityControllerBusinessProcessController 是Seam Application Framework的可选部分。 这些类只是提供了一些访问常用内置组件及这些组件方法的便利手段,它们能够减少一些键盘输入量,也为探索Seam内置丰富功能的初学者提供了一个非常好的跳板。

例如,这就是Seam注册实例中的 RegisterAction

@Stateless
@Name("register")
public class RegisterAction extends EntityController implements Register
{

   @In private User user;

   public String register()
   {
      List existing = createQuery("select u.username from User u where u.username=:username")
         .setParameter("username", user.getUsername())
         .getResultList();

      if ( existing.size()==0 )
      {
         persist(user);
         info("Registered new user #{user.username}");
         return "/registered.jspx";
      }
      else
      {
         addFacesMessage("User #{user.username} already exists");
         return null;
      }
   }

}

正如你所看到的一样,这不是什么惊世骇俗的提高...