论坛首页 Java企业应用论坛

构建自己的通用分页组件(下)

浏览 20373 次
精华帖 (0) :: 良好帖 (2) :: 新手帖 (3) :: 隐藏帖 (3)
作者 正文
   发表时间:2010-05-28   最后修改:2010-05-28

   在阅读本文之前,请先参考: 构建自己的通用分页组件(上)

1. 需求:

   在前一篇文章中,我们通过分析常见的分页需求,构建了一个通用的分页类Page和页面范围类PageScope,在此基础上完成了分页查询的前后台交互。但是取得当前页面的数据及其他分页页码信息后,最终还是要展示到页面上。
   一种常用的方法就是将分页查询后返回的page对象传递给页面,页面通过一些判断逻辑来进行相应的展示。但是这种方式不方便复用,即使将其单独独立出来,并在需要分页的页面分别导入,也不是很好的做法,因为在页面中写判断逻辑总是不太合适的。
   本文将以Jsp为例,使用Jsp的自定义标签,来实现一个页面分页标签处理程序,最终使得在需要分页的页面中能够简单、容易的复用。


2. 设计:
   首先我们来预览一下我们需要实现的功能,如下图:

分页标签

   为使设计更加灵活,从上述图片中,我们可以将这新展示内容抽象为:首页、末页、上一页、下一页、当前页、当前页之前的页数、当前页之后的页数、页码的链接地址、展示样式、分页信息等。而且,我们希望这些参数可由调用者自由设置,并根据调用者提供的参数不同,进行不同的展示,譬如:
分页标签
   根据这种实际需求,我们在PageTag类中定义如下属性:

public class PageTag extends TagSupport {

    private static final long serialVersionUID = 3441235160861303080L;
    // 首页
    private String homePage;
    // 末页
    private String lastPage;
    // 上一页
    private String previousPage;
    // 当前页之前的页数,默认为4。
    private int beforeNum = 4;
    // 当前页之后的页数,默认为5。
    private int afterNum = 5;
    // 分页对象
    private Page page;
    // 链接地址
    private String url;
    // 下一页
    private String nextPage;
    // 当前页码
    private String pageIndex;
    // 页面大小
    private String pageSize;
    // 外层div样式
    private String divClass;
    // 是否进行动态补足,默认为true。
    private boolean supplement = true;
}

   从上面的代码中可以看出,除了我们之前描述的需求外,我还定义了当前页码和页面大小的Url参数,这样做是为了可以在用户给定的Url后面动态加上当前页面页码及页面大小的参数。比如:用户设定的url链接为http://zhangshixi.iteye.com ,那么添加参数后的动态url为http://zhangshixi.iteye.com?pageIndex=6&pageSize=15 。另外,细心的读者还会发现,我们还定义了一个名为supplement的boolean型属性,用以标识是否对设定的当前页面之前和之后的页数进行动态补足。比如说:总页数为15页,当前为第2页,设定当前页之前显示4个页码,当前页之后显示5个页码,供10个页码。若不进行动态补足,将显示:1 2 3 4 5 6 7 这些页码,但若进行动态补足,由于前面页码不足,将从后面进行补充,反之亦然,那么补足后显示的页码将为:1 2 3 4 5 6 7 8 9 10

 

3. 实现:
   通过上面的分析和设计,下面我们继承Jsp提供的自定义标签TagSupport来实现自定义我们的分页标签PageTag。通过重写父类TagSupport的doStartTag()和doEndTag()方法,并在此方法中实现分页处理逻辑。

/**
 * 标签处理开始,构造一个存放分页信息的字符串缓冲区。
 * @return SKIP_BODY,忽略标签之间的内容。
 * @throws javax.servlet.jsp.JspException Jsp异常。
 */
@Override
public int doStartTag() throws JspException {
    buff = new StringBuilder();
    return SKIP_BODY;
}

/**
 * 标签实际分页处理逻辑。
 * @return EVAL_PAGE,按正常的流程继续执行Jsp页面。
 * @throws javax.servlet.jsp.JspException Jsp异常。
 */
@Override
public int doEndTag() throws JspException {
    if (page == null) {
        log.info("page is null.");
        return EVAL_PAGE;
    }

    // 写入开始DIV
    writeBeginDiv();
    // 写入分页信息
    writePageInfo();
    // 写入结束DIV
    writeEndDiv();
    // 记录日志
    writeDebugLog();
    // 输出到页面
    printToPage();

    return EVAL_PAGE;
}

    然后我们在分别完成各个子方法的实现即可。下面略举一二,最后将所有代码一并奉上。

/**
 * 写入实际的分页信息。
 * 调用者可自行设定是否显示首页、末页、上一页、下一页
 * 以及当前页面之前和之后的页数、是否进行动态补足等。
 */
private void writePageInfo() {
    int beforeCount = countBefore();
    int afterCount = countAfter();

    // 如果定义了动态补足,则对当前页之前和之后的页数进行动态补足。
    if (supplement) {
        beforeCount = fixBeforeCount(beforeCount, afterCount);
        afterCount = fixAfterCount(beforeCount, afterCount);
    }

    int index = page.getPageIndex();
    // 首页
    writeHomePage(index);
    // 上一页
    writePreviousPage(index);
    // 当前页之前的页码
    writeBeforePageIndex(index, beforeCount);
    // 当前页
    writeCurrentPageIndex(index);
    // 当前页之后的页码
    writeAfterPageIndex(index, afterCount);
    // 下一页
    writeNextPage(index);
    // 末页
    writeLastPage(index);
}
/**
 * 计算当前页之前的页数。
 * @return 当前页之前的页数。
 */
private int countBefore() {
    int beforeCount = 0;
    if (page.getPageIndex() - 1 > beforeNum) {
        beforeCount = beforeNum;
    } else {
        beforeCount = page.getPageIndex() - 1;
    }

    return beforeCount;
}
/**
 * 动态补足当前页之前的页数。
 * @param beforeCount 当前页之前的页数。
 * @param afterCount 当前页之后的页数。
 * @return 修正后的当前页之前的页数。
 */
private int fixBeforeCount(int beforeCount, int afterCount) {
    int totalNum = beforeNum + afterNum + 1;
    int useNum = beforeCount + afterCount + 1;

    if (useNum < totalNum) {
        int befores = page.getPageIndex() - 1;
        int margin = befores - beforeCount;
        if (margin > 0) {
            int needNum = totalNum - useNum;
            beforeCount += margin > needNum ? needNum : margin;
        }
    }

    return beforeCount;
}
/**
 * 写入首页信息,如果设定了显示首页。
 * @param index 当前页码。
 */
private void writeHomePage(int index) {
    if (homePage == null || homePage.isEmpty()) {
        return;
    }

    buff.append(LABEL_START);
    int homeIndex = 1;
    if (index > homeIndex) {
        writeUrlPageIndex(homeIndex, page.getPageSize(), homePage);
    } else {
        buff.append(homePage);
    }
    buff.append(LABEL_END);
}
/**
 * 写入当前页之前的页码。
 * @param index 当前页码。
 * @param beforeCount 前页之前的页数。
 */
private void writeBeforePageIndex(int index, int beforeCount) {
    int beginIndex = index - beforeCount < 0 ? 1 : index - beforeCount;
    for (int i = beginIndex; i < index; i++) {
        buff.append(LABEL_START);
        writeUrlPageIndex(i, page.getPageSize(), String.valueOf(i));
        buff.append(LABEL_END);
    }
}
/**
 * 将分页信息输入到页面上。
 */
private void printToPage() {
    try {
        pageContext.getOut().write(buff.toString());
    } catch (IOException ex) {
        log.error(ex.getMessage(), ex);
    }
}

 

4. 测试:
   针对此分页标签的实现,写了一个简单的测试代码来测试其正确性,如下:

public class PageTagTest {

    @Test
    public void writePage() throws JspException {
        PageTag instance = new PageTag();

        Page page = new Page(7, 10);
        page.setTotalData(75);

        instance.setPage(page);
        instance.setDivClass("divClass");
        instance.setHomePage("首页");
        instance.setLastPage("末页");
        instance.setPreviousPage("上一页");
        instance.setNextPage("下一页");
        instance.setPageIndex("page");
        instance.setBeforeNum(4);
        instance.setAfterNum(5);
        instance.setSupplement(false);
        instance.setUrl("http://zhangshixi.iteye.com");

        instance.doStartTag();
        instance.doEndTag();
    }
}

   最终写入到Jsp页面的内容如下:

<div class="divClass">
    <label><a href="http://zhangshixi.iteye.com?page=1">首页</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=6">上一页</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=3">3</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=4">4</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=5">5</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=6">6</a></label>
    <span>7</span>
    <label><a href="http://zhangshixi.iteye.com?page=8">8</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=9">9</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=10">10</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=11">11</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=12">12</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=8">下一页</a></label>
    <label><a href="http://zhangshixi.iteye.com?page=12">末页</a></label>
</div>

   限于篇幅,这里仅略举一例,当然,你也可写通过修改测试代码,来测试更多参数组合。至于在页面的最终展示样式,您可通过在外部css中,提供一个外层div的样式divClass来自行设定。在上传的代码中我会上传一个css的样式定义,来实现像本文开始时提供的效果,仅供参考,读者可在自己实际的项目中自行实现。


5. 如何使用?
   好了,通过上面的说明,我们已经详细介绍了分页标签的设计及实现,那么我们究竟如何将其使用在页面中呢?下面我讲意义说明:
首先,我们需要对自定义标签处理程序提供一个page-tag.tld配置文件,它定义了该标签的使用规则,参数规范等。如:

<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-jsptaglibrary_2_0.xsd">

    <tlib-version>1.0</tlib-version>
    <short-name>util</short-name>
    <uri>http://www.codingfarmer.com/tags</uri>
    <tag>
        <description>分页标签</description>
        <name>page</name>
        <tag-class>com.codingfarmer.util.page.PageTag</tag-class>
        <body-content>empty</body-content>
        <attribute>
            <description>首页</description>
            <name>homePage</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
            <type>java.lang.String</type>
        </attribute>
        <attribute>
            <description>分页对象</description>
            <name>page</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
            <type>com.codingfarmer.util.page.Page</type>
        </attribute>
        ........
    </tag>
</taglib>  

   限于篇幅,上面只略举了两个属性的定义,其余原理同上。在上传的源码中会有详细的配置,请下载参考。
   其次,在定义好自定义标签的配置文件后,我们还需要在web.xml中引入,如将去放到WEB-INF下,并在web.xml中配置如下:

<jsp-config>
    <taglib>
        <taglib-uri>pageTag</taglib-uri>
        <taglib-location>/WEB-INF/page-tag.tld</taglib-location>
    </taglib>
</jsp-config>

    最后,在需要分页的页面中我们可以插入以下代码,并根据实际需要设置相应的属性即可,如:

// 引入page-tag.tld文件的定义
<%@ taglib uri=" http://www.codingfarmer.com/tags" prefix="util" %>

// 分页显示
<util:page url="http://zhangshixi.iteye.com" page="${page}" 
           pageIndex=”pageIndex” homePage=”首页” lastPage=”末页” 
           previousPage="Previous" nextPage="Next" className="pageNav" />

   最终在页面上生成的分页效果为:

分页标签


6. 相关说明:
   至此,我们已经完成了一个通用的分页组件的设计与实现,这样,我们就可以将其收录到自己的工具箱中,使用时在项目中引入即可,从而大大简化了分页的处理及展示,也达到了良好的复用,简化了维护。
   下面上传的压缩包中,包含了自定义分页标签处理程序,测试类,还提供了一个仅供参考的css分页样式定义。还望大家哪呢个提出更好的建议和意见,以便改进,不胜感激。

   发表时间:2010-05-28   最后修改:2010-05-28
呵呵 跟我的差不多
http://crazy-j.iteye.com/admin/blogs/670686
0 请登录后投票
   发表时间:2010-05-28  
crazy.j 写道
呵呵 跟我的差不多http://crazy-j.iteye.com/admin/blogs/670686

大哥,你在开玩笑吧,咱没必要在这里乱贴链接吧?谢谢~!
0 请登录后投票
   发表时间:2010-05-28   最后修改:2010-05-28
zhangshixi 写道
crazy.j 写道
呵呵 跟我的差不多http://crazy-j.iteye.com/admin/blogs/670686

大哥,你在开玩笑吧,咱没必要在这里乱贴链接吧?谢谢~!

啊?我开什么玩笑了,我那里也有个分页的东西啊~
与分页相关的包含以下几个类:
Page:包含简单的分页参数以及查询结果,计算总页数等基本变量和操作,这是展现给底层数据查询的类型。
public class Page {
	
	protected Log logger = LogFactory.getLog(this.getClass());
	
	/**
	 * 构造方法
	 * @param pageSize 每页记录数
	 * @param pageIndex 当前页数
	 */
	public Page(Integer pageSize, Integer pageIndex, boolean countTotalPage){
		this.pageSize = pageSize;
		this.pageIndex = pageIndex;
		this.countTotalPage = countTotalPage;
	}

	// 每页记录数
	private Integer pageSize;
	
	// 当前页数
	private Integer pageIndex;
	
	// 总页数
	private Integer pageCount;
	
	// 总记录数
	private Integer totalRecordCount;
	
	// 是否需要计算总页数
	private boolean countTotalPage;
	
	// 本页记录
	private List record;

	/**
	 * @return the totalRecordCount
	 */
	public Integer getTotalRecordCount() {
		return totalRecordCount;
	}

	/**
	 * @param totalRecordCount the totalRecordCount to set
	 */
	public void setTotalRecordCount(Integer totalRecordCount) {
		this.pageCount = totalRecordCount % this.pageSize > 0 ? totalRecordCount / this.pageSize + 1 : totalRecordCount / this.pageSize;
		if(this.pageIndex > this.pageCount) {
			this.pageIndex = this.pageCount;
		} else if(this.pageIndex < 1) {
			this.pageIndex = 1;
		}
		this.totalRecordCount = totalRecordCount;
	}

	/**
	 * @return the pageIndex
	 */
	public Integer getPageIndex() {
		return pageIndex;
	}

	/**
	 * @return the pageSize
	 */
	public Integer getPageSize() {
		return pageSize;
	}

	/**
	 * @return the totalPageCount
	 */
	public Integer getPageCount() {
		return pageCount;
	}

	/**
	 * @return the countTotalPage
	 */
	public boolean isCountTotalPage() {
		return countTotalPage;
	}

	/**
	 * @return the record
	 */
	public List getRecord() {
		return record;
	}

	/**
	 * @param record the record to set
	 */
	public void setRecord(List record) {
		if(!countTotalPage && record.size() > pageSize) {
			this.record = record.subList(0, pageSize);
		} else {
			this.record = record;
		}
	}
}

PageEngine:仅包含表现层会使用到的是否有下一页一个状态,他的计算方式有2种,当分页操作不需要显示总页数的时候与需要显示总页数的时候是不一样的,不需要显示总页数的分页主要考虑表中数据过多时候count性能问题。针对这个类有一套标签用来显示,但是现在基本不用了。跟楼主不同的是,这套标签中由一个父级逻辑标签包含所有链接显示标签,父级标签处理逻辑参数后置于PageContext中,嵌套子标签从PageContext中获取参数负责显示“首页”,“上一页”等链接,每个链接一个标签,这样比较灵活。
public class PageEngine extends Page {
	
	// 是否有下一页
	private boolean hasNextPage;

	/**
	 * 构造方法
	 * @param pageSize 每页记录数
	 * @param pageIndex 当前页数
	 */
	public PageEngine(Integer pageSize, Integer pageIndex, boolean countTotalPage) {
		super(pageSize, pageIndex, countTotalPage);
	}
	
	/**
	 * 是否有下一页
	 * @return 是/否
	 */
	public boolean hasNextPage() {
	    return this.hasNextPage;
	}
	
	/**
	 * 是否有上一页
	 * @return 是/否
	 */
	public boolean hasPreviousPage() {
		return super.getPageIndex() > 1;
	}
	
	/*
	 * (non-Javadoc)
	 * @see com.funstool.platform.common.dao.Page#setTotalRecordCount(java.lang.Integer)
	 */
	public void setTotalRecordCount(Integer totalRecordCount){
		super.setTotalRecordCount(totalRecordCount);
		if(super.isCountTotalPage()){
			this.hasNextPage = super.getPageIndex() < super.getPageCount();
		}
	}
	
	/*
	 * (non-Javadoc)
	 * @see com.funstool.platform.common.dao.Page#setRecord(java.util.List)
	 */
	public void setRecord(List record){
		if(!super.isCountTotalPage()){
			this.hasNextPage = super.getPageSize() < record.size();
			if(this.hasNextPage) {
				super.setRecord(record.subList(0, this.getPageSize()));
			} else {
				super.setRecord(record);
			}
		} else {
			super.setRecord(record);
		}
	}

}

3.VelocityPageEngine:通过Velocity模板引擎生成html代码,当前使用最多的,因为非常灵活,而且jsp中使用简单。
public class VelocityPageEngine extends PageEngine {

	// 模板文件名
	private String templateName;
	
	private HttpServletRequest request;

	public VelocityPageEngine(String templateName, Integer pageSize, Integer pageIndex, HttpServletRequest request, boolean countTotalPage) {
		super(pageSize, pageIndex, countTotalPage);
		this.templateName = templateName;
		this.request = request;
	}
	
	@Override
	public String toString() {
		try {
			// 取得velocity的模版
			Template t = ((VelocityEngine)Platform.getInstance().getBean("VelocityEngine")).getTemplate(templateName, request.getCharacterEncoding());
			// 取得velocity的上下文context
			VelocityContext context = new VelocityContext();
			// 把数据填入上下文
			context.put("page", this);
			Map params = request.getParameterMap();
			StringBuilder url = new StringBuilder((String)request.getAttribute(MVCFacade.REQUEST_URL));
			String key = null;
			String[] values = null;
			url.append("?");
			for(Iterator keyIter = params.keySet().iterator(); keyIter.hasNext();){
				key = (String) keyIter.next();
				values = (String[]) params.get(key);
				if(key.equals("page_index")){
					continue;
				}
				for(int i = 0; i < values.length; i++){
					url.append(key);
					url.append("=");
					try {
						url.append(java.net.URLEncoder.encode(values[i], "UTF-8"));
					} catch (UnsupportedEncodingException e) {
						logger.warn(e);
					}
					url.append("&");
				}
			}
			
			if(url.lastIndexOf("?") != url.length() - 1){
				url.deleteCharAt(url.length() - 1);
			} else {
				url.append("r=");
			}
			context.put("url", url.toString());
			context.put("contextPath", request.getContextPath());
			StringWriter writer = new StringWriter();
			// 转换输出
			t.merge(context, writer);
			return writer.toString();
		} catch (Exception e) {
			logger.error("渲染VelocityPageEngine发生错误!", e);
			return null;
		}
	}

}

jsp中使用:
<div class="page">${page}</div>

每当编写jsp代码到这里时候会觉得非常贴心~

web层某类中有2个方法可以直接在Action中调用,用来创建分页引擎。
	/**
	 * 创建分页引擎对象
	 * @return 分页引擎
	 */
	public PageEngine createPageEngine() {
		Integer pageSize = this.getIntegerParameter("page_size");
		Integer pageIndex = this.getIntegerParameter("page_index");
		if(pageSize == null){
			pageSize = DEFAULT_PAGE_SIZE;
		}else if(pageSize > MAX_PAGE_SIZE){
			pageSize = MAX_PAGE_SIZE;
		}
		PageEngine page = new PageEngine(pageSize, pageIndex == null ? 1 : pageIndex, countPage);
		request.setAttribute(PAGE_NAME, page);
		return page;
	}
	
	/**
	 * 创建模版分页对象
	 * @param templateName 模版文件名
	 * @return 分页引擎
	 */
	public PageEngine createPageEngine(String templateName) {
		Integer pageSize = this.getIntegerParameter("page_size");
		Integer pageIndex = this.getIntegerParameter("page_index");
		if(pageSize == null){
			pageSize = DEFAULT_PAGE_SIZE;
		}else if(pageSize > MAX_PAGE_SIZE){
			pageSize = MAX_PAGE_SIZE;
		}
		PageEngine page = new VelocityPageEngine(templateName, pageSize, pageIndex == null ? 1 : pageIndex, request, countPage);
		request.setAttribute(PAGE_NAME, page);
		return page;
	}


PaginationProcessor接口:DAO的成员变量,DAO通过方法参数Page对象传递给该接口的实现,对于不同的数据库不同的ORM框架可以有不同的实现,这里就不多写了。
1 请登录后投票
   发表时间:2010-05-28  
lz的分页组件是基于get请求吧,我见过很多种,其实用处不大。我们的分页其实包含很多查询条件(当然你也可以把这些条件第一次查询时放到session中)。当查询条件包含中文的时候,可能会乱码。
0 请登录后投票
   发表时间:2010-05-28  
建议点击下一页时, 提交表单, 不应是链接方式
0 请登录后投票
   发表时间:2010-05-28  
这玩艺,地球人都知道了。
0 请登录后投票
   发表时间:2010-05-28  
不好!太复杂了。就这么一个分页搞得这么复杂。
0 请登录后投票
   发表时间:2010-05-28  
写得真不好.~
0 请登录后投票
   发表时间:2010-05-28  
ilove2009 写道
lz的分页组件是基于get请求吧,我见过很多种,其实用处不大。我们的分页其实包含很多查询条件(当然你也可以把这些条件第一次查询时放到session中)。当查询条件包含中文的时候,可能会乱码。

session是记录用户级别的信息,将查询条件放入session中,似乎有些乱用吧?当查询条件包含中文的时候,只需对url参数进行编码即可。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics