keyword:分頁 緩存 eXtremeTable oscache
引子:這幾天在弄一個關于頁面的分頁,查了一下網上的資料,大都不合要求,要么就是說怎么在數據庫這個層面上如何實現,暈,有了hibernate我用那么費勁翻身么.看到一個用的比較多的方案是做了一個Page工具類,實現諸如getBooks(),getNextPage(),看了一下底層實現居然是"select * from book",嚇死偶了,要是有1千萬條記錄那不是要吐血啊,這哪叫分頁啊,這該叫殺人不見血啊. 一氣之下在jbuilder下建了一個項目就叫FuckPage :)
好了,言歸正傳,本文主要說的是關于在展示層一些常用的方案和實現,目錄如下:
- 手工實現分頁
- 用eXtremeTable標簽實現自動分頁
- 用oscache緩存jsp,提高性能
第一.自己實現一個工具類PageBean完成所有分頁工作.
本分頁實現概覽:Struts + hibernate
PageBean負責兩部分內容,一是要在頁面顯示的業務信息,是一個ArrayList;另一個是邏輯控制信息,諸如是否有下一頁,上一頁等等.
PageBean代碼如下:
public class PageBean {
int currentPage = 1;//當前頁:Action控制
int totalPages = 0;//總頁數 :自己運算
int pageRecorders = 5; //每頁記錄數,默認為5,可以在初始化的時候修改//總數據數
int pageStartRow = 0; //每頁的起始數
int pageEndRow = 0; //每頁顯示數據的終止數
boolean hasNextPage = false; //是否有下一頁:自己運算
boolean hasPreviousPage = false; //是否有前一頁 :自己運算
List objList = new ArrayList();//存放欲展示的對象列表
int totalRows;//總記錄數,由底層service提供
//是否有上一頁
public boolean isHasPreviousPage() {
return (currentPage > 1?true:false );
}
//共有多少頁,service只提供有多少條記錄,多少頁數由PageBean自己運算
public int getTotalPages() {
return (totalRows/pageRecorders ==0?totalRows/pageRecorders:totalRows/pageRecorders+1);
}
public int getCurrentPage() {
return currentPage;
}
public int getPageEndRow() {
return pageEndRow;
}
//是否有下一頁
public boolean isHasNextPage() {
return (currentPage < this.getTotalPages() ? true:false);
}
public int getTotalRows() {
return totalRows;
}
public int getPageStartRow() {
return pageStartRow;
}
public int getPageRecorders() {
return pageRecorders;
}
public void setObjList(List objList) {
this.objList = objList;
}
public void setHasPreviousPage(boolean hasPreviousPage) {
this.hasPreviousPage = hasPreviousPage;
}
public void setTotalPages(int totalPages) {
this.totalPages = totalPages;
}
public void setCurrentPage(int currentPage) {
this.currentPage = currentPage;
}
public void setPageEndRow(int pageEndRow) {
this.pageEndRow = pageEndRow;
}
public void setHasNextPage(boolean hasNextPage) {
this.hasNextPage = hasNextPage;
}
public void setTotalRows(int totalRows) {
this.totalRows = totalRows;
}
public void setPageStartRow(int pageStartRow) {
this.pageStartRow = pageStartRow;
}
public void setPageRecorders(int pageRecorders) {
this.pageRecorders = pageRecorders;
}
public List getObjList() {
return objList;
}
public PageBean() {}
public void description() {
String description = "共有數據數:" + this.getTotalRows() +
"共有頁數: " + this.getTotalPages() +
"當前頁數為:" + this.getCurrentPage() +
" 是否有前一頁: " + this.isHasPreviousPage() +
" 是否有下一頁:" + this.isHasNextPage() +
" 開始行數:" + this.getPageStartRow() +
" 終止行數:" + this.getPageEndRow();
System.out.println(description);
}
}
注意,我沒有在PageBean里放具體的業務邏輯,諸如getBooks()等,目的很簡單,具有通用性,業務邏輯由另一個業務類實現BookService,BookService獲得的業務數據都放在了PageBean的ArrayList里.
BookService代碼如下:
public class BookService {
private static Logger log = Logger.getLogger(BookService.class.getName());
public BookService() {
}
/**
* 獲得book列表
* @param pageBean PageBean:返回的對象存在pageBean里
*/
public static void getBooks(PageBean pageBean) {
String infoSql = "from Book";//獲得業務信息
String countSql = "select count(*) from Book";//獲得控制信息
Session session = null;
try {
session = DBUtil.currentSession();
Query query = session.createQuery(infoSql);
query.setFirstResult((pageBean.getCurrentPage()-1)* pageBean.getPageRecorders());//起始頁
query.setMaxResults(pageBean.getPageRecorders());//每頁記錄數
pageBean.getObjList().clear();
for (Iterator it = query.iterate(); it.hasNext(); ) {
Book po = (Book)it.next();
BookVo vo = new BookVo();
BeanUtils.copyProperties(vo,po);
pageBean.getObjList().add(vo);
}
session = DBUtil.currentSession();
query = session.createQuery(countSql);
int totalRecords = ((Integer)query.list().get(0)).intValue();
pageBean.setTotalRows(totalRecords);
}
catch (Exception e) {
e.printStackTrace();
System.out.println("數據庫異常" + e.toString());
}
finally {
try {
if (null != session) {
session.close();
}
}
catch (HibernateException ex) {
ex.printStackTrace();
}
}
}
}
在Struts的Action中調用service,返回一個PageBean給展示頁面
Action代碼如下:
1 public class PageListAction extends Action {
2
3 public PageListAction() {}
4
5 ArrayList arrayList = new ArrayList();
6
7 public ActionForward execute(ActionMapping mapping,ActionForm form,HttpServletRequest request,HttpServletResponse response) throws Exception {
8 String action;
9 PageBean pageBean = null;
10 action = request.getParameter("action");
11 if(action == null || action.equals("null")) { //第一次讀取數據
12 pageBean = new PageBean();
13 } else {//用戶選擇上一頁或者下一頁
14 if(action == "nextPage" || action.equals("nextPage")) {
15 pageBean = (PageBean)request.getSession().getAttribute("pageBean");
16 pageBean.setCurrentPage(pageBean.getCurrentPage()+1);
17 }else if(action == "previousPage" || action.equals("previousPage")) {
18 pageBean = (PageBean)request.getSession().getAttribute("pageBean");
19 pageBean.setCurrentPage(pageBean.getCurrentPage()-1);
20 }else if(action == "targetPage" || action.equals("targetPage")){//指定頁
21 pageBean = (PageBean)request.getSession().getAttribute("pageBean");
22 System.out.println("targetPage=" + request.getParameter("targetPage"));
23 //這里根據需要可以對填寫的目標頁進行判斷,不要大于最大頁數[此處省略]
24 pageBean.setCurrentPage(Integer.parseInt(request.getParameter("targetPage")));
25 }
26 }
27 if(null == pageBean) throw new Exception("獲得PageBean異常");
28 BookService service = new BookService();
29 service.getBooks(pageBean);
30 pageBean.description();
31 request.getSession().setAttribute("pageBean",pageBean);
32 request.setAttribute("result",pageBean.getObjList());
33 return(mapping.findForward("success"));
34 }
35 }
在本Action中判斷了可能出現的三種情況:
- 用戶選擇了"上一頁"
- 用戶選擇了"下一頁"
- 用戶手工輸入了指定的某一頁
這里有點感覺不爽的是必須hard coding,但是不這么做感覺暫時也想不出什么好的辦法來,畢竟一個PageBean不可能封裝所有的細節,如果你有更好的方式請指點哦 :)
好了,到了我們呼之欲出的展示頁面了 :)
show.jsp代碼如下
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ page contentType="text/html; charset=gb2312" language="java"%>
<html:html locale="true">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<script language="javaScript">
function go(){
try{
var targetValue = document.getElementById("targetPage").value;
parseInt(targetValue);
alert(targetValue);
}catch(e){
alert("請正確填寫目標頁");
return;
}
if(targetValue == null || targetValue == ''){
alert("請填寫目標頁");
return;
}
window.location = "/fuck/pageList.do?action=targetPage&targetPage="+targetValue;
}
</script>
</head>
<body>
<logic:present name="pageBean">
共有數據總數<bean:write name="pageBean" property="totalRows"/>;
共分<bean:write name="pageBean" property="totalPages"/>頁,
當前是第<bean:write name="pageBean" property="currentPage"/>頁
</logic:present>
<table border="1">
<tr><th>書名</th><th>作者</th><th>價格</th></tr>
<logic:present name="result">
<logic:iterate id="book" name="result">
<logic:present name="book">
<tr>
<td><bean:write name="book" property="name" /></td>
<td> <bean:write name="book" property="author" /></td>
<td><bean:write name="book" property="price" /></td>
/tr>
</logic:present>
</logic:iterate>
</logic:present>
</table>
<logic:present name="pageBean">
<logic:equal name="pageBean" property="hasNextPage" value="true">
<html:link page="/pageList.do?action=nextPage">nextPage</html:link>
</logic:equal>
<logic:equal name="pageBean" property="hasPreviousPage" value="true">
<html:link page="/pageList.do?action=previousPage">PreviousPage</html:link>
</logic:equal>
<input type="text" name="targetPage" id="targetPage"/>
<input type="button" value="go!" size="2" onclick="go();"/>
</logic:present>
</body>
</html:html>
是否有上一頁或者下一頁,全部根據PageBean里的邏輯值動態判斷.
這個頁面沒什么可說的,你可以根據自己的情況調整就OK了
第二. 用eXtremeTable標簽實現自動分頁
上面的方案大家已經看出來了,實際上是每一次用戶點擊一個頁面都會查詢數據庫,這可以算是既是優點也是缺點,優點是數據庫不用一次查詢出所有的數據,在高數據量的情況下尤其如此,缺點就是和數據庫的交互次數有點多了,不過這個完全看你的業務策略了,如果用戶大多數情況下就是看沒幾條的記錄,你又何必把全部數據給他取出來呢? 當然,在這里我們就說說一次取出全部數據,然后讓標簽幫助我們自動分頁,終于可以偷懶了,你所要做的僅僅是取出所需要的業務數據而已,其他的就交給eXtremeTable標簽來完成就OK了.
eXtremeTable標簽的下載,安裝和文檔請參看官方網站
public static List getBooks() {
log.debug("execute getBooks method!");
String infoSql = "from Book";//獲得業務信息
Session session = null;
List rtnList = new ArrayList();
try {
session = DBUtil.currentSession();
Query query = session.createQuery(infoSql);
for (Iterator it = query.iterate(); it.hasNext(); ) {
Book po = (Book) it.next();
BookVo vo = new BookVo();
BeanUtils.copyProperties(vo,po);
log.debug("vo = [" + vo + "]");
rtnList.add(vo);
}
}
catch (Exception e) {
e.printStackTrace();
System.out.println("數據庫異常" + e.toString());
}
finally {
try {
if (null != session) {
session.close();
}
}
catch (HibernateException ex) {
ex.printStackTrace();
}
}
return rtnList;
}
代碼如下:
public class PageListWithTagAction extends Action {
public ActionForward execute(ActionMapping actionMapping, ActionForm actionForm, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
BookService service = new BookService();
//我這里把數據放到session中,相當于做了緩存,根據你的業務策略也可以不用這么做
//如果session中沒有,則從數據庫中查詢
if(null == httpServletRequest.getSession().getAttribute("result")){
List result = service.getBooks();
httpServletRequest.getSession().setAttribute("result",result);
}
return (actionMapping.findForward("success"));
}
}
- jsp[show.jsp代碼如下,留心里面的標簽使用方法]
<body bgcolor="#ffffff">
<ec:table tableId="fuck"
items="result"
action="${pageContext.request.contextPath}/pageListWithTag.do"
imagePath="${pageContext.request.contextPath}/jsp/images/table/*.gif"
title="Books"
width="60%"
rowsDisplayed="5"
locale="zh_CN"
cellpadding="1"
cellspacing="1"
border="1"
method="post"
showPagination="false"
filterable="false"
>
<ec:exportXls fileName="Book.xls" tooltip="導出Excel">
</ec:exportXls>
<ec:exportPdf fileName="Book.pdf" tooltip="導出pdf" headerColor="blue" headerBackgroundColor="red" headerTitle="Book"></ec:exportPdf>
<ec:row highlightRow="true">
<ec:column property="name" title="書名">
<a href="${pageContext.request.contextPath}/bookDetail.do?bookID=${fuck.id}&bookName=${fuck.name}">${fuck.name}
</a>
</ec:column>
<ec:column property="author" title="作者">
<!--here can't use 'result.author'-->
${fuck.author}
</ec:column>
<ec:column property="price" title="價格" cell="currency" format="$###,###,##0.00"/>
<ec:column property="date" title="日期" cell="date" format="yyyy年MM月dd日">
</ec:column>
</ec:row>
</ec:table>
</body>
感覺如何?你不用再畫你的頁面了,不用再畫table了,這點我特別喜歡,因為我自己畫的東西都比較難看,畢竟我的美工功夫不夠 :( "自從有了eXtremeTable吃嘛嘛香" :)
具體的標簽使用方法請參考官方文檔的Manual,說明還是比較詳細的.
第三.用oscache緩存你的頁面為了提高頁面的速度我們想了很多辦法,比如
預編譯的辦法,以及把你常用的數據放到內存里,是的,除了用內存我們還能想到用什么辦法呢,恩,我想以后cpu的緩存也特別大的話我們的下一個方案肯定就是把數據全部放到cpu里得了,哈哈,展望一下 :)
言歸正傳,
oscache的廣告我就不做了,差不多地球人都知道了,這里僅僅提供了一個想法,我想這確實是一個不錯的方案,它提供了2個途徑使用,一是通過tag的方式,可能也是用的最多的方式,另一個便是調用API,當然就可以在任何想調用的地方使用了.另外還有一個特別不錯的功能就是有策略的刷新數據.這是一個useful的方式,比如你做了增刪改操作那么數據庫的數據已經發生變化了,你可以通知緩存來更新數據,方式是通過key或者group.
下面是幾個摘自FAQ里的幾個常用Example
<cache:cache time="600">
<%= myBean.getTitle() %>
</cache:cache>
<cache:cache key="foobar" scope="session">
<%= myBean.getTitle() %>
</cache:cache>
<cache:cache>
<% try { %>
<%= myBean.getTitle() %>>
<% } catch (Exception e) { %>
<% application.log("Exception occurred in myBean.getTitle(): " + e); %>
<cache:usecached />
<% } %>
</cache:cache>
上面的Example3可以實現在數據庫當機的情況下從緩存里讀取數據展示,顯得更加友好
不知你注意到了沒有,在oscache官方展示的例子里的jsp都有一個我稱之為毛病的東西,或者說是困惑,那就是都用了
<%=
.%> 這種方式,感覺有點別扭,畢竟這種使用方式對于別人我不知道,反正對于我來說用的比較少,在以前的使用中我記得只有使用xml數據島的時候用過這種方式,其他情況下很少用=的方式來打印出一些動態的數據,更常見的可能是如下這樣的情況:
<cache:cache key="dispInfo" groups="disInfo" time="1200">
<%
BookService service = new BookService();
List list = service.getBooks();
request.setAttribute("list",list);
%>
</cache:cache>
<c:forEach items="${list}" var="item">
<c:out value="${item.id}"/> == <c:out value="${item.name}"/><br />
</c:forEach>
不幸的是,這個一廂情愿的做法并不被oscache支持,除了第一次能夠顯示數據,下一次就顯示不了了.也許oscache所謂的緩存jsp代碼就是指緩存諸如<%=%>的方式才是jsp代碼,其他的java型的就不被支持了?可能是理解不夠,繼續研究吧,希望有知道的不吝賜教 :)
恩,就是這么多,關于頁面的緩存和分頁以及標簽.歡迎有這方面的更好的想法來交流.
本人不才,可能有很多地方理解不夠深入,見笑了 :)