作者:张纪豪(转载请注明出处)
(一)摘要与设计思想
海水无量,只取一瓢。大量的数据显示,分段提取是通用的做法,是的,这叫分页技术。
<t:p/>,对于WEB程序员,这种标签格式不会陌生。t:代表自定义标签库;p:分页标签。两个字母(具体的说它是6个字符组成)便可以搞定J2EE平台下所有分页功能。当然可以扩充,例如显示总记录数、总页数、当前页数、每页记录数等信息。
效果截图1:
效果截图2:
是不是很诱人?那好吧,笔者带大家开始艰难之旅吧。
艰难?呵呵,不用害怕,还是那套机制,只是不同的封装。
本文将从图书、新闻两个实体数据显示来详细介绍这种通用分页技术。
J2EE项目架构设计:
最初微软的Asp脚本语言给动态WEB应用程序开发带来广阔的天地,由此互联网更加精采,不过笔者没有用过Asp来架构过大型项目,因为听说中关村的程序员大部分都有颈椎病预期都是因为陷进了微软的泥潭(逗乐!)。对于那种将所有程序代码和网页显示代码混在一个文件中且脱离不了过程化的编程方法,用在大型项目的构建着实有些可怕。因此J2EE规范中将程序与页面代码分离是一次伟大的革新,并在后来演变成多层结构分层治之的思想,随后微软的C#效仿之,这才初步将程序员慢慢解放。
典型的J2EE项目分为模型-视图-控制三层,即MVC(Model-View-Controller),模型是整个项目中最复杂部分,它除了有核心的数据操纵技术,还需要紧密的业务逻辑,所以模型层又可包含模型领域、数据访问层、服务层;控制层有一个经典的框架是Struts,熟悉它的Action就知道控制层的作用;视图层较容易些,例如最广泛使用的JSP。通过MVC概念就知道原来说用JSP做项目就可看出自己还没有入行,现在已经不是微软的ASP时代,因为在J2EE里根本不用JSP就可以强大的WEB项目(例如页面用velocity模板技术而不用JSP)。这也就意味着J2EE项目里的视图不再局限于网页,它可能是Excel、PDF、Desktable Window等等。
本文阐述的分页技术围绕一个含数据领域为五层的小项目来展开描述,控制层和模型层都交给Spring管理,属真正的SSH集成开发模式:
表现层为采用JSP技术;
控制层采用当今最热门Struts(版本为1.x,不是Struts2) ,虽然我发现它的牙都快掉光了,但还是选择它以更能熟练掌握本分页方法;
模型层中含有服务层、数据库访问层和数据领域层,其中:
持久层采用Hibernate(版本3.2),Hibernate管理领域对象的配置采用注解方式,与xml方式配置功能是一样。
控制层和模型层全交给Spring容器管理。
数据库管理系统不太关心,因为现在是面向对象编程,你给Hibernate怎么配置,Hibernate就怎么实现。
下一节将介绍工程建立、数据领域模型以及它的映射到数据库必须的配置(上述本例采注解方式)、page对象的封装……
(二)项目工程基础和领域模型
一、工程基础:
1、我们先在MyEclipse开发工具里建一个WEB项目,并将各包建立如下:
com.zhangjihao.domain 存放领域对象,需要持久化到数据库的
com.zhangjihao.bean 存放非持久化到数据库的bean
com.zhangjihao.dao 数据访问层
com.zhangjihao.service 服务层
com.zhangjihao.web WEB层,其中web包下还有struts、filter、taglib等子包
com.zhangjihao.util 自己的工具包
2、准备所需的jar包:
再将Struts1.2、Spring2.5、Hibernate3.2、Log4j、Sun和Apache的工共组件、数据库驱动等等所需的jar包都拷入到WebRoot/WEB-INF/lib目录,并Build Path到MyEclipse中(一般MyEclipse会自动Build Path)。
3、xml文件的准备:
web.xml中加上Struts和Spring配置:
<!-- 配置Spring并在WEB容器中实例化之 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:beans.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 利用spring解决乱码 --> <filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 利用spring解决hibernate延迟加载问题 --> <filter> <filter-name>openSessionInViewFilter</filter-name> <filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class> </filter> <filter-mapping> <filter-name>openSessionInViewFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 配置Struts1 --> <servlet> <servlet-name>action</servlet-name> <servlet-class>org.apache.struts.action.ActionServlet</servlet-class> <init-param> <param-name>config</param-name> <param-value>/WEB-INF/struts-config.xml</param-value> </init-param> <load-on-startup>0</load-on-startup> </servlet> <servlet-mapping> <servlet-name>action</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping>
在struts-config.xml中加上(第四篇将有将更详细的内容):
<!--将action交给Spring管理--> <controller> <set-property property="processorClass" value="org.springframework.web.struts.DelegatingRequestProcessor"/> </controller>
准备一个Spring的xml配置文件:
由于web.xml里已经写了beans.xml,我们就用它作为Spring的配置文件,由于Hibernate也交给了Spring管理,我们就不需要给Hibernate写配置文件,而是将Hibernate配置信息写在beans.xml:
<!--注意这些名称空间,很初学者使用Spring时出错就是因为这里没有声明--> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd "> <!-- 注入一个数据源,我们这里用的是MySQL数据库 --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost:3306/数据库名?useUnicode=true&characterEncoding=UTF-8"/> <property name="driverClassName" value="org.gjt.mm.mysql.Driver"/> <property name="username" value="数据库用户名"/> <property name="password" value="数据库密码"/> <!-- 连接池启动时的初始值 --> <property name="initialSize" value="1"/> <!-- 连接池的最大值 --> <property name="maxActive" value="500"/> <!-- 最大空闲值,当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 --> <property name="maxIdle" value="2"/> <!-- 最小空闲值,当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 --> <property name="minIdle" value="1"/> </bean> <!-- Spring集成Hibernate配置 --> <bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation. AnnotationSessionFactoryBean"> <!--这个是用注解方式来配置实体映射--> <property name="dataSource" ref="dataSource"/> <property name="annotatedClasses"> <list> <value>com.zhangjihao.domain.Book</value> <value>com.zhangjihao.domain.News</value> </list> </property> <property name="annotatedPackages"> <list> <value>com.zhangjihao.domain</value> </list> </property> <property name="hibernateProperties"> <value> hibernate.dialect=org.hibernate.dialect.MySQL5Dialect hibernate.hbm2ddl.auto=update hibernate.show_sql=true hibernate.format_sql=false </value> </property> </bean> <!-- 配置事务管理并打开Spring声明式事务功能 --> <bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean> <tx:annotation-driven transaction-manager="txManager"/> <!-- 打开Spring注解依赖注入功能 --> <context:annotation-config/> <!-- 打开Spring扫描创建Bean功能 --> <context:component-scan base-package="com.zhangjihao"/> <!-- 打开AOP拦截功能 --> <aop:aspectj-autoproxy/>
再在数据库管理系统中开一个库。通过上面的工作,我们的工程基础基本准备好。这些虽然偏主题,但笔者作事偏谨,故啰嗦了这么多,或许对刚接触SSH集成开发的朋友还是有很大的帮助。
二、实体搭建(Entity Bean):
我们这个主题在前面讲过围绕图书和新闻两个实体来描述分页方法,所以建立这个实体,并将Hibernate映射配置借助JPA标准在实体方法中通过注解来配置:
1、图书实体Book.java
package com.zhangjihao.domain; import java.io.Serializable; import java.util.Date; import javax.persistence.Basic; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Lob; import javax.persistence.Temporal; import javax.persistence.TemporalType; @Entity public class Book implements Serializable{ private static final long serialVersionUID = 5251288153182073422L; //**编号**// private Integer id; //**书名**// private String bookname; //**作者**// private String author; //**出版社**// private String publisher; //**价格**// private Float price; //**ISBN号**// private String isbn; //**日期**// private Date pubDate = new Date(); //**描述**// private String description; /构造方法 public Book() {} public Book(Integer id, String bookname, String author, String publisher, Float price, String isbn, Date pubDate) { this.id = id; this.bookname = bookname; this.author = author; this.publisher = publisher; this.price = price; this.isbn = isbn; this.pubDate = pubDate; } /Getter @Id @GeneratedValue public Integer getId() { return id; } @Column(nullable=false,length=100) public String getBookname() { return bookname; } @Column(nullable=false,length=30) public String getAuthor() { return author; } @Column(length=100) public String getPublisher() { return publisher; } @Column(nullable=false) public Float getPrice() { return price; } @Column(length=100) public String getIsbn() { return isbn; } @Temporal(TemporalType.DATE) public Date getPubDate() { return pubDate; } /**支持大字段**/ @Lob @Basic(fetch=FetchType.EAGER,optional=false) @Column(columnDefinition="LONGTEXT NOT NULL") public String getDescription() { return description; } /Setter public void setId(Integer id) { this.id = id; } public void setBookname(String bookname) { this.bookname = bookname; } public void setAuthor(String author) { this.author = author; } public void setPublisher(String publisher) { this.publisher = publisher; } public void setPrice(Float price) { this.price = price; } public void setIsbn(String isbn) { this.isbn = isbn; } public void setPubDate(Date pubDate) { this.pubDate = pubDate; } public void setDescription(String description) { this.description = description; } /Override Method @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final Book other = (Book) obj; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id)) return false; return true; } }
2、新闻实体(News.java)
package com.zhangjihao.domain; import java.io.Serializable; import java.util.Date; import javax.persistence.Basic; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Lob; import javax.persistence.Temporal; import javax.persistence.TemporalType; @Entity public class News implements Serializable{ private static final long serialVersionUID = 928281566591424585L; //**编号**// private Integer id; //**新闻标题**// private String title; //**新闻来源**// private String rootin; //**创建日期**// private Date createdate; //**新闻内容**// private String content; /构造方法 public News() {} public News(Integer id, String title, String rootin, Date createdate) { this.id = id; this.title = title; this.rootin = rootin; this.createdate = createdate; } /Getter @Id @GeneratedValue public Integer getId() { return id; } @Column(nullable=false,length=100) public String getTitle() { return title; } @Column(length=100) public String getRootin() { return rootin; } @Temporal(TemporalType.DATE) public Date getCreatedate() { return createdate; } @Lob @Basic(fetch=FetchType.EAGER,optional=false) @Column(columnDefinition="LONGTEXT NOT NULL") public String getContent() { return content; } /Setter public void setId(Integer id) { this.id = id; } public void setTitle(String title) { this.title = title; } public void setRootin(String rootin) { this.rootin = rootin; } public void setCreatedate(Date createdate) { this.createdate = createdate; } public void setContent(String content) { this.content = content; } /Override Method @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((id == null) ? 0 : id.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; final News other = (News) obj; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id)) return false; return true; } }
当然,这仅仅是为了描述分页技术,对于真正的应用,这种数据库设计是不完美的,例如最常用到图书分类、新闻分类、发布人、有效性设定等信息,在此都作省略。
编写java实体Bean时,都要养成几个习惯:
_实现Serializable接口;
_覆盖hashCode()和equals(Object obj)方法;
_不再用基本类型定义字段;
_定义了常用字段的构造方法时,一定要定义一个空的构造方法。
上面两个实体类都有两个构造方法,一个空的,一个是用于列表显示时用的。我们知道,当分页显示图书时,其每本书的内容介绍(description字段)是不需要显示出来的,因为当用户点某本书查看更多详细的书内容介绍时才需要显示图书描述(description字段),而往往在数据库中description字段中数据库甚至大于其它所有字段内容之和,所以从性能角度考虑,在分页显示时图书列表则不需要description字段,大家可仔细看看上面的构造方法。
还需要注意类名上和get方法上面的注解,它们功能和*.hbm.xmk文件内容差不多,不熟悉的朋友去看看JPA的书籍。
三、 Page Bean
下面是重点的Page类封装(Page.java)。
对于一个分页项,其中有三个重要的属性分别是:总记录数量、每页记录数量、当前页,有了这三个属性便可以计算出总页数,当然有必要再加一个url属性,因为第n页和n±1页对于服务器来讲是两个不同的页面,url便是用于页面的导向,好,下面看看详细的代码:
package com.zhangjihao.bean; /** * 分页Bean * @author 张纪豪 * @version 0.1 * Build Time Mar 19, 2007 */ public class Page { /**URL上页码参数**/ public static String pageNumberParameterName = "pageIndex"; /**当前页码**/ private int pageIndex; /**每页记录数**/ private int pageSize = 10; /**总记录数**/ private int totalCount; /**页码上的地址**/ private String url; /** * Index starts from 1, 2, 3... */ public Page(int pageIndex) { this.pageIndex = pageIndex; } /** * Index starts from 1, 2, 3... */ public Page(int pageIndex, int pageSize) { this.pageIndex = pageIndex; this.pageSize = pageSize; } /** * 带URL地址构造方法,最常用的一种 * @param pageIndex 当前页 * @param pageSize 每页记录数 * @param url URL地址 */ public Page(int pageIndex, int pageSize, String url) { this.pageIndex = pageIndex; this.pageSize = pageSize; this.url = url; } public void setPageIndex(int pageIndex) { this.pageIndex = pageIndex; } public int getPageIndex() { return pageIndex; } public int getTotalCount() { return totalCount; } public void setTotalCount(int totalCount) { this.totalCount = totalCount; } public int getPageSize() { return pageSize; } /** * 开始页 * @return */ public int getFirstIndex() { return (pageIndex-1) * pageSize; } /** * 最后页 * @return */ public int getLastIndex() { int n = getFirstIndex() + pageSize; if (n > totalCount) n = totalCount; return n; } public int getPageCount() { if (totalCount==0) return 0; return totalCount / pageSize + (totalCount % pageSize==0 ? 0 : 1); } public boolean isEmpty() { return totalCount==0; } public boolean getHasPrevious() { return pageIndex > 1; } public boolean getHasNext() { return pageIndex < getPageCount(); } public String getUrl() {return url;} public void setUrl(String url) {this.url = url;} }
对于Page类的写法非常灵活,有点将查询结果封闭到该对象上,甚至有点将SQL语句封装里面,笔者认为只要达到功能,越简单越好。
事实上有了Page对象,整个思路就全部明白,后面的工作无非是将Page对象中的数据填充和显示出来