1. 需求:
在实际项目开发中,分页是我们常见的操作,在一般数据展示的列表页,都会使用到数据分页。分页时,在每个页面上只需取得该页面展示的数据及列出其他的页码即可,这样可以以合适的粒度来获取页面展示的数据,避免不必要的数据的传输。
在软件的分层构架中,实现一个供前后台交互用的分页组件,已成为每个项目必不可少的潜在需求。本文将在实际项目中分页需求的基础上,讨论并实现一个通用的分页组件,然后,可将其收录到自己的工具箱中,在不同的项目中导入复用即可。
2. 构建Page类:
我们知道,在实际分页时,前台调用者在调用后台业务方法分页取得数据时,需告诉业务方法当前取第几页的数据以及每页取多少条数据;并希望返回的结果集中不仅包含当前页面所需要的数据列表,还要包含总数据量和总页面数。这样在前台展示的时候,就可以在展示当前页面数据时,又能将所有的分页信息展示给用户,以供用户作为访问其他页面数据的导航之用。因此,我们可以构造一个Page类,来表示分页信息,如下:
public class Page { // 当前页面的页数 private int pageIndex; // 页面大小 private int pageSize; // 数据总量 private int totalData = -1; // 剩余数据量 private int surplusData; // 页面总量 private int totalPage; // 是否仅取第一页 private boolean firstPage; // 是否可以工作。默认为false,只有设置了数据总量才为true。 private boolean ready = false; }
该Page类包含了当前页数、每页的数据量、总数据量、剩余数据量、总页面量、是否仅取第一页以及该分页对象是否已经准备好,可以正常工作的标志。这样,调用者在调用后台业务方法进行分页查询数据时,只要构造一个指定当前页数pageIndex和页面大小pageSize的分页对象page,作为参数传递给后台业务方法即可。因此,根据实际需要,我们提供以下构造Page对象的构造方法:
// 默认当前页面页数为第一页 private static final int DEFAULT_PAGE_INDEX = 1; // 默认页面大小为10 private static final int DEFAULT_PAGE_SIZE = 10; /** * 以默认当前页面和页面大小构造一个分页对象。 * 其中,默认当前页数为1,默认页面大小为10。 */ public Page() { this.pageIndex = DEFAULT_PAGE_INDEX; this.pageSize = DEFAULT_PAGE_SIZE; } /** * 以指定的当前页面页数和页面大小构造一个分页对象。 * @param pageIndex 当前页数,若参数值不大于0,则使用默认值1。 * @param pageSize 页面大小,若参数值不大于0,则使用默认值10。 */ public Page(int pageIndex, int pageSize) { this.pageIndex = pageIndex > 0 ? pageIndex : DEFAULT_PAGE_INDEX; this.pageSize = pageSize > 0 ? pageSize : DEFAULT_PAGE_SIZE; } /** * 以指定的页面大小构造一个表示第一页的分页对象。 * @param pageSize 页面大小,若参数值不大于0,则使用默认值10。 * @return 构造好的第一页分页对象。 */ public static Page newFirstPage(int pageSize) { Page page = new Page(1, pageSize); page.setFirstPage(true); page.setTotalData(pageSize); return page; }
无参构造方法Page()将构造一个默认当前为第1页,每页取10条数据的分页对象;Page(int pageIndex, int pageSize)可以构造一个指定当前页数和页面大小的分页对象;newFirstPage(int pageSize)为一静态方法,此方法可以以指定的页面大小构造一个表示只取第一页数据的分页对象,这种方式将不会计算数据总量和页面总量,出返回第一页的数据外,不做任何处理,以满足我们在一些首页只取前几条数据的需求。
至此,Page类的基本属性已定,下面我们看一下如何添加一些方法,利用这个Page类来实现前后台的交互。
3. 添加方法:
从上面的需求分析中可知,根据调用者传递的page对象参数,后台业务方法应根据此参数查询出当前页面的数据列表、总数据量和总页面数返回给调用者。那么,我们抛开数据持久层具体实现技术不管,除了在底层采用分页技术查询出当前页面的数据外,如果调用者不是只取第一页,那么还应该查询出总的数据量,并设置到page对象中,由于知道了总数据量和每页显示的数据量,我们就能够计算出总页面数,剩余数据量等信息。因此,我们希望,在设置总数据量之后,page对象能够自动将这些信息计算好,所以我们在Page类中提供了设置总数据量的方法如下:
/** * 设置数据总量。在使用时,需提前调用此方法进行设置。 * 当数据总量设置好之后,会计算页面总量、修正当前页面页数、计算剩余数据量, * 并设置该分页对象已经准备好,可以正常工作。 * @param totalData 数据总量。 */ public void setTotalData(int totalData) { this.totalData = totalData; this.totalPage = this.totalData / pageSize + (this.totalData % pageSize == 0 ? 0 : 1); if (this.pageIndex > this.totalPage) { this.pageIndex = this.totalPage; } this.surplusData = this.totalData - (this.pageIndex - 1) * this.pageSize; this.ready = true; }
在设置完总数据量之后,分页对象page就能正常工作了。持久层在设置完数据总量后,必须取得当前要查询数据的起始行号和结束行号,以便对数据库进行分页查询。因此,我们在Page类中还必须提供获取当前分页的页面范围的方法,如下:
/** * 获取当前分页对象的页面范围,包含当前页面的起始行和结束行。 * 如果未先调用setTotalData()方法设置数据总量,则会抛出运行时异常。 * @return 当前分页对象的页面范围。 * @throws java.lang.IllegalArgumentException 异常。 */ public PageScope getPageScope() throws IllegalArgumentException { if (!ready) { throw new IllegalArgumentException("Page has not seted data amount."); } if (this.pageIndex > this.totalPage) { return null; } PageScope scope = new PageScope(); int startLine = (this.pageIndex - 1) * this.pageSize; int endLine; if (this.surplusData < this.pageSize) { endLine = startLine + this.surplusData - 1; } else { endLine = startLine + this.pageSize - 1; } if (startLine < 0) { startLine = 0; } if (endLine < 0) { endLine = 0; } scope.setStartLine(startLine); scope.setEndLine(endLine); return scope; }
PageScope类作为标识当前页面的页面范围信息。它只有两个属性:起始行号和结束行号。如下:
public class PageScope { // 起始行号 private int startLine; // 结束行号 private int endLine; }
这样,后台业务方法通过对指定页面数据的分页查询和对时局总量的设定,把调用者所需要的页面数据和其他页面信息包装到page对象中返回,就实现了我们通用的分页需求。
4. 如何使用?
在介绍完分页类Page和页面范围类PageScope后,我们来看一下,如何使用自定义的分页组件来简化我们的实际开发。下面我们以分页查询用户列表为例,来说明我们自定义的通用分页组件的使用:
业务层在用户业务接口UserService中提供的分页查询用户列表的方法queryUsers(Page page);
public List<User> queryUsers(Page page) throws ServiceException;
调用者如果想查询第3页、每页查询15条用户信息,则可使用如下调用方式:
Page page = new Page(3, 15); UserService userService = new UserServiceImpl(); List<User> usertList = userService.queryUser(page);
这样调用者就取到了第3页15条的用户信息,而且此时的page对象中,已经包含了其他页面的信息,包括总页数,总数据量等,我们可根据这些信息来展示其他页码。
如果只想取前5条数据,而不关心其他其他页面信息,则可使用:
Page page = Page.firstPage(5); List<User> usertList = userService.queryUser(page);
说明: 当不是分布式的应用时,前后台部署在同一个JVM上,这样通过前台传递的page对象的引用,后台设置好页面信息后,实际已经修改了前台传递的page对象,这样就不必再将page对象传回前台调用者。
但是在分布式的环境中,后台在对page对象操作后,必须与结果集userList一起传递给调用者,并且Page类和PageScope类要实现序列化接口。
5. 测试:
下面提供了一个简单的测试,来说明自定义的分页组件的使用正确性:
public class PageTest { /** * 测试构造一个仅表示第一页的分页对象。 */ @Test public void newFirstPage() { Page page = Page.newFirstPage(10); PageScope scope = page.getPageScope(); assertTrue(page.isFirstPage()); assertEquals(10, page.getTotalData()); assertEquals(1, page.getTotalPage()); assertEquals(1, page.getPageIndex()); assertEquals(10, page.getPageSize()); assertEquals(0, scope.getStartLine()); assertEquals(9, scope.getEndLine()); } /** * 测试构造一个指定当前页数和页面大小的分页对象。 */ @Test public void newPage() { Page page = new Page(2, 10); page.setTotalData(55); PageScope scope = page.getPageScope(); assertFalse(page.isFirstPage()); assertEquals(55, page.getTotalData()); assertEquals(6, page.getTotalPage()); assertEquals(2, page.getPageIndex()); assertEquals(10, page.getPageSize()); assertEquals(10, scope.getStartLine()); assertEquals(19, scope.getEndLine()); } }
6. 相关说明:
本文通过对实际项目中分页的需求,来构建一个通用的分页组件,以便在项目中复用并简化了项目中分页的开发工作。
所有的源代码和测试代码已上传,仅供大家参考。
如果有更好的建议和意见,还望大家不吝赐教,以便我改进,谢谢!
在下文中我会再写一个自定义的Jsp分页标签,以便在页面中通用处理分页页码。