摘要:本文主要介绍了如何利用Frails框架进行快速开发。在本文中我们的目标是架构起一个Spring+JSF+Hibernate的项目,并且实现后台代码。您会发现,原本仅仅实现一个Spring框架就要话很长时间的“痛苦”过程,在Frails框架和模板的帮助下,几乎喝杯咖啡的功夫就可以搞定了。您甚至会发现,不需要写任何SQL或者数据库相关代码就能够实现几乎可以覆盖全部的数据库操作需求。通过本文,您一定会喜欢上在Frails框架帮助下快速开发的感觉。


Frails简介
Frails是SourceForge.net上的一个开源项目,其项目管理开发是由华中科技大学IBM俱乐部JAVA组与软件学院学生合作完成。Frails框架诞生的目的就是帮助开发者快速开发J2EE项目。更多基本信息您可以查看http://www.sourceforge.net/projects/frails(英文),http://frails.hexiao.cn/(中文)。

我们现在假设您对现在主流的J2EE框架Spring,JSF,Hibernate有所了解。那么您会发现这些框架的配置与实现是相当复杂的,然而相当多的情况下我们不需要用到其中高灵活性的配置。就拿JSF来说,JSF的页面导航配置实际上是一种没有必要的灵活,实际上我们更喜欢的将A页面导航到B页面的规则简单的约定,而不是每个都要手动配置;验证过程也是很麻烦的,相对于将验证代码零星的嵌入到页面逻辑中,我们更希望看到的是有统一的验证中心,这样更符合软件工程的原则;对于数据库操作来说,通常我们的操作都是增删查改四种操作,每个操作都会有很多重复的“垃圾”代码,这也阻碍了快速开发的目标;Spring更方便的注射,也能够提高开发速度。上述问题在Frails中有良好的解决,并且保持了与原始框架一样的高灵活可配置性,甚至提高了灵活性,或者是提供了一种更优秀的解决方案。这里要强调的是,Frails不是其他框架的一个简化版本,它只是通过开发者和框架之间的一种约定或者是“默契”,代办了其他框架累赘、重复的工作。如果你想要高灵活的配置,Frails同样适合。

动手实践
下面我们将展示一个建立项目的过程。这里的项目需求是实现一个简单的留言簿,目的就是通过最简单的例子最大化的覆盖到Frails的特性。

搭建项目
首先要有个IDE比较好,我使用的是Eclipse与WTP插件,这样开发起来会很方便,当然你要是不想用IDE或者用别的也一样可以,Frails是和IDE无关的。

最方便的是Frails为一般的项目提供了一个模板FrailsTemplate(这个模板在2.0包里的Samples下可以找到)。这个模板已经包含了Spring+Hibernate+JSF的全部配置。我们仅仅需要对这个模板进行做些适合自己的修改就可以快速的搭建起一个项目来了。

我们新建一个WEB项目,将FrailsTemplate的WEB-INF拷贝到自己的项目中来,并且在scr中建立四个包,分别为actions,domain,dao,domain.entities。这样基本上一个项目的结构就我们接下来介绍这些行为的意义:

WEB-INF/lib目录下是整个项目需要的所有JAR包。其中Frails需要的JAR就是frails4jsf1.1.jar。如果你没有别的什么特殊需求,例如要使用Icefaces的JSF组件,那么这个lib的JAR文件足够你开发了。你看看里面还包括了tomahawk-1.1.3.jar呢。但是里面没有包含facelet,如果你想用facelet的话直接添加进来就可以了。如果你仅仅想急着些个JSF页面完成老板的任务,那么你这些东西都不用管了,直接考过来就是了。

WEB-INF下的几个配置文件可能需要改动:

Jdbc.properties
Jdbc.properties里面包含的是你的数据库配置。实际上applicationContext.xml里数据库有关的配置都关联到此。

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost/test?useUnicode=true&characterEncoding=GBK
jdbc.username=root
jdbc.password=password

jdbc.driverClassName对应的是数据库驱动。正对不同的数据库填写不同的驱动就可以了。这里的是MySQL。
Jdbc.url是数据库链接地址,一般不需要更改。但是你要用别的数据库还是得改成相应的数据库URL。最后的characterEncoding是数据库编码,这里选用的是GBK,你也可以根据你的需要设置为UTF-8或者别的。
Jdbc.username是数据库用户名,需要根据你自己的设置更改。
Jdbc.password是数据库密码,需要根据你的设置更改。

applicationContext.xml
基本的应用上下文配置,一般不需要修改。

faces-config.xml
由于Frails框架的原因,原本复杂的faces-config.xml你现在可以一个字不用写。

frails-config.xml
Frails框架配置。简单的来说你可以不做任何修改。Frails将根据这里的配置(如果没有的话就是默认配置),识别项目中的ManagedBeans,Validators,Converters,页面,资源目录等。具体说明可以在Frails项目包中的Frails User Guide中的Configrations找到相应的说明。这里简单的认为什么都不写就好了,我们等下按照一个默认配置的规范来继续我们的项目开发就OK了。

hibernate.cfg.xml
Hibernate配置。这个配置是必须修改的。至少我们在每写一个Hibernate实体的时候,就应该在SessionFactory里添加一条<mapping class="domain.entities.ClassName"/> class里面对应了一个实体的类名。我们等下也会提到。
如果你并不是使用MySQL数据库的话,需要把数据库的别名修改成你数据库的相应设置:<propertyname="dialect">org.hibernate.dialect.MySQLDialect</property>。
<property name="hibernate.hbm2ddl.auto">update</property>是说每次启动Hibernate都会自动检查,如果原来数据库中没有表则建立新的表,如果有了则更新原有表。

spring-beans.xml
Spring框架配置。这个配置是必须修改的。这里主要是配置所有的dao,Spring按照这个配置进行依赖注入。不要担心,我们的配置也之是写点点东西,等下详细说明。

现在项目的基本配置就完成了,虽然还有些要根据实际的设计实现增加些配置,但是大体的工作已经完成了。您看,根本就没有花很多时间。下面让我们快速进入到实际的开发实现过程中来。


实现一个项目

现在该轮到解释刚才建立的四个包了(当然了,你也完全可以不这么做,包名只是对类的一种功能上的分类,完全可以根据自己的需要更改为其他的名字):actions包里面包含的是所有的ManagedBeans;dao里面包含所有的数据库相关业务接口;domain包里是数据库业务实现;domain.entities包里是所有的实体类。下面结合我们的例子来介绍。
例子:我们要实现一个留言簿,notebook.jsp页面显示出留言列表,并在下方显示一个用户名和内容输入框,点击提交后将留言储存到数据库里。

后台(业务逻辑)代码设计与实现

1 建立实体
我们设计是每条留言记录对应一个实体:在包domain.entitis下建立Note.java,并且按照Hibernate的Annotation规范编写,使得Note.java这个类会按照Hibernate的方式映射到数据库中。(关于Hibernate相关内容请查看Hibernate相关文档,这里不再赘述),代码如下:

package  domain.entities;

import  javax.persistence.Entity;
import  javax.persistence.GeneratedValue;
import  javax.persistence.GenerationType;
import  javax.persistence.Id;

@Entity
public   class  Note  {
    
    
/**
     * 要数据库为每个留言建立一个ID
     
*/

    @Id
    @GeneratedValue(strategy
= GenerationType.AUTO)
    
long  id;
    
    
/**
     * 留言者姓名
     
*/

    String poster;
    
    
/**
     * 留言内容
     
*/

    String content;

    
public  String getContent()  {
        
return  content;
    }


    
public   void  setContent(String content)  {
        
this .content  =  content;
    }


    
public   long  getId()  {
        
return  id;
    }


    
public   void  setId( long  id)  {
        
this .id  =  id;
    }


    
public  String getPoster()  {
        
return  poster;
    }


    
public   void  setPoster(String poster)  {
        
this .poster  =  poster;
    }


}

同时,不要忘了,你应该在hibernate.cfg.xml的<session-factory>里加上对本实体映射声明<mapping class="domain.entities.Note"/>。

2实体的数据库操作接口

在dao包内设计出所有实体对饮的数据库业务接口,最佳实践是,每个实体对应一个接口,并且使用ClassnameDao的形式命名。所以我们这里的为NoteDao。所则上我们应该在本接口中声明所有的和Note类相关的数据库方法,但是就像前面提到过了的,不同实体的数据库业务基本上都是增、删、查、该,无非是在操作的对象上有不同而已。所以在实现的阶段,我们要实现每个接口,将是非常累赘的事情——反复的写例如开关数据库的“垃圾”代码。好在我们有了Java 5的泛型,以及Frails的GenericDao<T>,我们可以省了好多事情。GenericDao基本上已经帮我们声明了所有可能要用到的数据库方法,一般情况下我们仅仅之需要让实体的Dao继承自此就可以了,不用写任何代码。记得继承的时候要声明泛型的类型。所以NoteDao接口代码如下:

package dao;

import net.sf.frails.hibernate.GenericDao;
import domain.entities.Note;

public interface NoteDao extends GenericDao<Note> {

}

3 实现实体数据库操作接口

下面是实现每个dao了。你可能会说,尽管本例子中只有一个dao,但实际的项目中实体的数目将会很多,并且每个实体内都有不少的方法要实现,这样一个个的写代码将是多大的负担啊!幸运的是,再一次Frails解决了这个问题,同样,我们让所有的实现类继承自一个支持泛型的GenericDaoSupport<T>。

GenericDaoSupport实现了GenericDao<T>接口,已经实现了几乎所有可能要用到的数据库方法。NoteDaoImp代码如下,我们还是什么都没有写:

package dao;

import java.util.List;
import net.sf.frails.hibernate.GenericDaoSupport;
import domain.entities.Note;

public class NoteDaoImp extends GenericDaoSupport<Note> implements NoteDao{
}

这个时候我们就要修改spring-beans.xml里的配置了。在<Beans>里为每个daoImp加入一个声明<bean id="noteDao" parent="dao" class="dao.NoteDaoImpl"></bean>

这个地方parent属性指的是在applicationContext.xml里帮助Dao对象得到hibernate的Session的抽象Bean 的名字。
并且在<bean id="servicesImp" class="domain.ServicesImp">内为每个dao加入属性:<property name="noteDao" ref="noteDao"></property>。这里ServicesImp就是下面要提到的业务逻辑实现。所以整个的<Beans>的结构如下:

<beans>    
    
<bean id="dao" abstract="true">    
        
<property name="sessionFactory" ref="sessionFactory"/>
    
</bean>
    
<bean id="noteDao" parent="dao" class="dao.NoteDaoImp">
    
</bean>
    
<bean id="services" parent="transactionProxy">
        
<property name="target" ref="servicesImp"/>
        
<property name="transactionAttributes">
            
<props>                
                
<prop key="*">PROPAGATION_REQUIRED</prop>
            
</props>
        
</property>
    
</bean>
    
<bean id="servicesImp" class="domain.ServicesImp">
        
<property name="noteDao" ref="noteDao"></property>
    
</bean>
</beans>

4 提供统一的业务逻辑接口

接下来我们要将所有的业务逻辑包装起来。在一个接口中声明所有对外提供的业务逻辑方法,这里我们起名叫做Services。这样做的目的是统一对外提供业务逻辑,当我们设计页面逻辑的时候就可以完全不用关心业务逻辑实现了,所以页面设计人员和后台设计人员可以分离,各自仅仅关心自己的领域显然有助于高效的开发。再者,如果我们需要更改业务逻辑方法的话,仅仅修改这个接口和其实现就可以了,使得代码有很高的维护性。代码如下:

package domain;
import java.util.List;
import domain.entities.Note;

public interface Services {
    
    
public void saveNote(Note note);
    
    
public List<Note> getAllNotes();

}

5 实现统一数业务逻辑接口

当然了,我们现在就要实现的Services里声明的所有数据库方法:在domain包的ServiceImp,这里才是要真正写代码的地方(别急,每个方法不超过两行就搞定了)。这个类有所有dao的引用,Spring会依照上面在spring-beans.xml的配置注入所有引用的实例,实际上我们调用的是GenericSupport的方法,而这些方法基本上覆盖了所有的业务逻辑需求。通过继承GenericDaoSupport<T>的方法如下,足够你用了,万一不行使用里面的HQL的查询支持的方法吧。

我们这个例子的ServicesImp代码如下,这样看来我们仍然没有写什么代码啊:

package domain;
import java.util.List;
import dao.NoteDao;
import domain.entities.Note;

public class ServicesImp implements Services{
    NoteDao noteDao;

    
public List<Note> getAllNotes() {
        
return noteDao.listAll();
    }


    
public void saveNote(Note note) {
        noteDao.save(note);
    }


}


这样,我们的后台代码,也就是业务逻辑代码已经编写完毕了。下面我们开始写前台。


前台(页面)逻辑代码实现

1 页面与ManagedBean

我们在actions包里添加一个叫做NotebookAction的类,这个类自动的将成为notebook.jsp(等下建立)页面的ManagedBean,负责处理本页面的逻辑。根据Frails的默认配置,在actions包下的以页面名字开头(首字母大写)加上Action结尾的类将自动成为该页面的ManagedBean。如果你不喜欢这个后缀(Action),你也可以根据自己的需要该成别的,方法是在frails-config.xml的<frails-config>里加上:

<mbean-package>actions</mbean-package>这里是对应的ManagedBean所在的包名称。
<mbean-suffix>Action</mbean-suffix>这里是所有ManagedBean的后缀。

按照Frails 2.0的规范,我们还应该在相应的Bean上显示的标注些annotation。具体代码如下:

package action;

import net.sf.frails.bean.annotations.DefMbean;
import net.sf.frails.bean.annotations.ScopeType;

@DefMbean(scope
=ScopeType.REQUEST)
public class NotebookAction {

}

您所要做的就是在类声明的前面加上@DefMbean(scope=ScopeType.REQUEST)。其中scope是Bean的范围,默认的是REQUEST。每个页面的ManagedBean都应该尽量使用REQUEST范围,这样做是有很大好处的,我们将在最后一个部分具体介绍。在给这个Bean里添加页面所需要的属性和方法。我们让所有的ManagedBean继承自一个ServicesAction,在ServicesAction里包含了对Services的声明。按照Frials框架,在Services声明前注明@SpringBean后,Spring就可以自动注入了。

这样继承的好处是,我们在做页面测试的时候,只需要把ServicesAction的引用换成一个假的Services实现,只是模拟下数据库的结果就省去大量的反复启动数据库的时间了。

package action;

import net.sf.frails.bean.annotations.SpringBean;
import domain.Services;
import domain.ServicesImp;

public class ServicesAction {

    
/**
     * 若果是测试页面的话,去掉annotation换成模拟的实现就好了:
     *  Services services = new MockServices();
     *  这里MockServices是对Services接口的实现,
     *  但是逻辑代码并不是真的操作数据库,而是直接返回一些东西,方便页面测试。
     
*/

    @SpringBean
    Services sevices;
}

 

package action;

import java.util.ArrayList;
import java.util.List;

import net.sf.frails.bean.annotations.DefMbean;
import net.sf.frails.bean.annotations.Prop;
import net.sf.frails.bean.annotations.ScopeType;
import domain.entities.Note;

@DefMbean(scope
=ScopeType.REQUEST)
public class NotebookAction extends ServicesAction{
    
    
/**
     * 存放已有的Note列表
     
*/

    @Prop
    List
<Note> noteList = new ArrayList<Note>();

    
/**
     * 存放一个新的Note
     
*/

    @Prop
    Note note 
= new Note();
    
    
/**
     * 增加一个Note
     * 
@param newNote 要增加的Note
     
*/

    
public void addNote(Note newNote)
    
{
        
    }

}

这个Bean里没有setter和getter方法,这个并不是没有给出完整的代码,而是根本不用写!Frials知道为每个属性增加setter和getter是多么乏味的事情,虽然elipse能够自动处理,但是Frials是和IDE无关的,并不假设快速开发一定要建立在功能强大的IDE上(但是实际上JSF是暗含有这样的假设的)。只需要在属性前面加上@Prop,这个属性就相当于自动加上了setter和getter方法。

更重要的是,我们在写对应的页面的时候,EL表达式可以更加简练:在引用对应本页面的ManagedBean的时候,我们不需要再写这个Bean的名字,取而代之的是一个美元符号。所以,我们在写datatable标签的时候,value的值写成 #{$.noteList}这样就好了。现在来写JSF页面吧,除了EL表达式简化了外,和原来的JSF写法没有什么区别,记住名字的对应就好了。

这里要说明的一点是Frails的EL表达式可以传递参数,形式为#{$.method['arg1','arg2'... ]}。并且Frails提供了注入支持,我们甚至可以直接在页面上通过EL表达式调用Services方法,比如我们这里要实现删除一个Note,那么可以这样写dataTable:

<h:dataTable value="#{$.noteList}" var="note">
            
<h:column>
                .
            
</h:column>
            
<h:column>
                
<h:commandButton action=#{$.Services.delete['note']}>
<!-- BUTTON NAME -->
</h:commandButton>
            
</h:column>
</h:dataTable>

另外页面的导航也简化成直接写要导航到页面的名字了。如action="a",则直接导航到a.jsp。

2 输入验证

假设我们需要验证验证是否输入了用户名,并且用户名的长度在4-12之间。输入内容应该大于12。当然,我们完全可以按照JSF原有的方式在页面里嵌入验证的组件。但是,验证和页面逻辑混杂在一起不是件好事情,随着需要验证的内容加大,代码变得难以维护,并且验证需求的变更——这个是可能的可会导致很大的损失。在Frails里,对属性的验证都在ManagedBean里属性声明时候处理。也就是说,我们声明一个属性的时候同时说明需要满足什么条件的时候这个属性才会接受值,否则我们就提示错误信息,或者转到某个专门用来报错的页面。如果我们要在NotebookAction里验证输入的note,具体的做法有两种:

一是使用annotation在当前Bean里写验证方法,一般用于对一般类型的验证。要做的就是在属性前面加上@ValidateXXX(message="error message"),message里存放错误信息。
例如我们要验证一个Email:

@Prop
    @ValidateEmail(message
="error.message")
    String email;

或者要验证字符串,要求不为空,最小长度为3:

@Prop
    @ValidateString(required 
= true, minLen = 3)
    String name;

还有@ValidateNumber,@ValidateDate。如果这些基本的还不能满足你验证需求,可以使用一个方法来抓们验证属性,例如我们要用一个叫validateMethod的方法验证一个叫username的字符串,直接在Prop加上属性(validator="Method"):

@Prop(validator="validate")
String username;
public void validate(String username) {
if(username == null || username.length() == 0{
//DO SOMETHING TO REPROT ERROR
}

}


二是使用验证Bean,用于对类的属性域的验证。回到我们notebook的例子中来,我们要验证一个note,可以在note属性前加上@ValidateBean以及参数:

@Prop
    @ValidateBean(fields
={
    @Field(name
="poster",required=true,
            validateNumber
=@ValidateNumber(max=12,min=8,message="Poster Name     too long or to short"),
            message
="You must input your name"),
    @Field(name
="content",required=true,
            validateString
=@ValidateString(minLen=8,message="You have     to put more words here"),
            message
="You must input the content")
    }
)
    Note note 
= new Note();

其中每一个@Feild对应了一个类中要验证的属性。Name对应的是属性的名称,必须与你在类中属性声明一致;required代表是否不为空。后面还可以跟上若干validateXXX嵌套,每个层面上都可以有message来显示错误。当然了,其中的以部分这里也可以被标注为使用方法来验证。

也可以将验证Bean单独写到一个专门的验证类上,只需要在被验证的属性前加上@ValidateBean,并且在里面指明用来验证类的别名就可以了:

@Prop
    @ValidateBean(name
="noteValidator")
    Note note 
= new Note();

然后我们新建一个类来负责验证note。在类前面加上@BeanValidator标注,其fiels与恰面提到的一样,只是注意在BeanValidator的name要和前面被验证属性上的匹配。代码如下:

import net.sf.frails.bean.validator.BeanValidator;

@BeanValidator(name 
= "infoValidator", fields = {
        
//THE SAME WITH VALIDATORBEAN
}
)
public class Validator {

}

以上我们实现了统一的验证过程。经过实践后您会发现将验证代码从页面分离开后会带来多么大的好处。这些请您亲自去体验吧。

3 ManagedBean范围及设计模式

在实际的项目中,页面肯定不会只有一个。页面和页面之间的需要传递的数据,以前往往是放在Session里面,因为JSF内建的导航机制都是POST请求。这就使得我们不得不将些ManagedBean设置为Session范围,但是这样做并不是那么必要,会造成一些麻烦。最好的设计是,每个页面的ManagedBean都是Request范围,他们通过简单的GET请求互相传递一些数据的主键,而每个ManagedBean自己负责根据这些主键将相应的信息提取出来。这样页面与页面之间就通过类似接口一样关联起来,一旦他们之间传递的主键约定好了,页面就可以相对分离开;同时,这些页面也是支持浏览器收藏夹收藏的。这样我们只需要少量的,基本上是一个Session范围的Bean保存当前用户的信息,一个Application范围的Bean保存应用的某些属性,其余的都是Request范围的Bean来实现整个页面架构了。

有了Frails中@PreRender将一个方法标注为在JSF组件树渲染周期前调用,@Param将一个属性标注为GET请求参数,通过这两个annotation 的结合使用,我们能很轻松的实现GET请求。

假设我们要新增加一个页面叫note.Jsp,用来显示一条留言的详细内容。他的ManagedBean(当然就是NoteAction了)接受一个long id作为GET请求参数,并且在渲染出组件树前事先从数据库里根据id提取出note的内容。

package action;

import net.sf.frails.bean.annotations.DefMbean;
import net.sf.frails.bean.annotations.Param;
import net.sf.frails.bean.annotations.PreRender;

@DefMbean
public class NoteAction {

    @Param(name 
= "noteID")
    
private long id;
    
    @PreRender
    
private void preRender()
    
{
        
if( id != 0 )
        
{
            
//GET INFO
        }
else{
            
//HANDLE WITH NO GET PARAM
        }

    }

    
    
//OTHERS
}

上面Param里的name属性就是GET参数的名字,并且可以自动的将String参数类型转换成其他的基本类型。GET参数的定义可以简单的在页面上使用<h:OutputLink>,这个标签对应的是一个GET请求,在内嵌入<f:param>就是请求的参数了。

<h:outputLink value="note.jsp" >
                    
<h:outputText value="查看详细信息"></h:outputText>
                    
<f:param name="noteID"/>
        
</h:outputLink>

请注意outputLink的属性value就是要导航的页面,这里Frials没有对其进行任何的简化,必须填写要导航到的页面的全名。Param里的name属性就是GET参数的名字,必须和noteAction里的@Param属性一致。


总结:

我们通过一个简单的例子,基本上了解了Frails框架模板快速搭建一个Spring+Hibernate+JSF的项目过程。首先,我们按照Frails的默认配置快速的完成了各种设置;接下来我们使用Frails对Dao的支持类在没有写任何Hibernate或者SQL代码的情况下完成了数据库业务的方法;然后我们了解了页面上Frails简化的EL表达式;还有相当重要的验证方法;最后我们了解了下通过实现GET请求来使得页面之间相对松耦合的设计。希望这样的介绍能带您进入到Frails框架的奇妙世界中来,我相信当您亲自体验Frails的魅力后,一定爱不释手!