應(yīng)該IT168寫(xiě)的專(zhuān)稿:
http://publish.itpub.net/j/2008-01-24/200801241020641.shtml
使用JasperReport與iBATIS開(kāi)發(fā)Web報(bào)表
JasperReport是一種采用純Java實(shí)現(xiàn)的快速且非常流行的生成報(bào)表的類(lèi)庫(kù)。而對(duì)于任何的報(bào)表方案,取得數(shù)據(jù)并傳遞給報(bào)表引擎是其中最重要且最值得關(guān)心的方面。但遺憾的是,在這方面JasperReport本身似乎有一定的不足。而如今的很多Java應(yīng)用程序,采用數(shù)據(jù)獲取框架來(lái)進(jìn)行數(shù)據(jù)的匹配與動(dòng)態(tài)生成SQL。例如iBATIS數(shù)據(jù)映射框架。當(dāng)然,如果只是使用JasperReport獲取數(shù)據(jù)及管理數(shù)據(jù)的默認(rèn)機(jī)制的話(huà),不足以與現(xiàn)成的數(shù)據(jù)框架進(jìn)行很好的平衡。但可喜的是,可以通過(guò)使用傳遞給JasperReport一個(gè)數(shù)據(jù)庫(kù)的連接進(jìn)行代替,當(dāng)然這種連接可以通過(guò)使用XML進(jìn)行非常方便的管理與配置。
源代碼下載:http://cid-7b377ace522ff6c7.skydrive.live.com/self.aspx/iBatisJasper/iBatisJasper.rar
一、準(zhǔn)備工作
與Hibernate類(lèi)似,iBATIS也是一個(gè)ORM解決方案,不同的是兩者各有側(cè)重。Hibernate提供了Java對(duì)象到數(shù)據(jù)庫(kù)表之間的直接映射,開(kāi)發(fā)者無(wú)需直接涉及數(shù)據(jù)庫(kù)操作的實(shí)現(xiàn)細(xì)節(jié),實(shí)現(xiàn)了一站式的ORM解決方案。而iBATIS則采取了另一種方式,即提供Java對(duì)象到SQL(面向參數(shù)和結(jié)果集)的映射實(shí)現(xiàn),實(shí)際的數(shù)據(jù)庫(kù)操作需要通過(guò)手動(dòng)編寫(xiě)SQL實(shí)現(xiàn)。
iBATIS是又一個(gè)O/R Mapping解決方案,j2ee的O/R方案真是多,和Hibernate相比,iBATIS最大的特點(diǎn)就是小巧,上手很快。如果你不需要太多復(fù)雜的功能,iBATIS是能滿(mǎn)足你的要求又足夠靈活的最簡(jiǎn)單的解決方案。在本文的示例中,采用Spring+JSF+iBATIS的模式進(jìn)行示例的開(kāi)發(fā)。所使用的lib如下圖所示:

圖1.所使用的jar包
二、在iReport中可視化定制模板
定制報(bào)表格式有二種方式,一種就是寫(xiě)jrxml文件,其實(shí)就是xml文件,只不過(guò)是后綴名不一樣罷了。另一種方式更直接,就是生成一個(gè)JasperDesign類(lèi)的實(shí)例,在japsperDesign中自己定義模板。jrxml文件也是通過(guò)一個(gè)JRXmlLoad加載過(guò)來(lái),轉(zhuǎn)成JasperDesign類(lèi)的實(shí)例。也就是說(shuō)寫(xiě)jrxml文件還需要進(jìn)行解析,加載?,F(xiàn)實(shí)中我們使用的報(bào)表一般格式比較固定,因而可以通過(guò)先使用iReport工具生成模板,再加載解析的方式。這種方式簡(jiǎn)單,而且可見(jiàn)性強(qiáng)。
iReport做為一個(gè)優(yōu)秀的報(bào)表設(shè)計(jì)器,有著功能非常強(qiáng)大的特性。作為開(kāi)源的Java程序,不但有適合于Windows安裝的應(yīng)用程序,同時(shí),還提供完全開(kāi)放的源代碼,可供參考及原理分析。在本文中,主要通過(guò)圖形界面中的模板設(shè)計(jì),以及與數(shù)據(jù)庫(kù)的連接等一系列的操作,來(lái)介紹如何定制一定要求的報(bào)表模板。
通過(guò)iReport可初見(jiàn)化的圖形界面,可以設(shè)計(jì)出各種各樣的簡(jiǎn)單或復(fù)雜的報(bào)表。通過(guò)iReport的這種可視化界面設(shè)計(jì),可以為JasperReport提供優(yōu)秀的報(bào)表模板,而無(wú)須去理解或是掌握那些復(fù)雜的XML語(yǔ)法。如此則可以Web報(bào)表開(kāi)發(fā)節(jié)省大量的開(kāi)發(fā)時(shí)間。
在進(jìn)行iReport模板設(shè)計(jì)之前,需要編寫(xiě)JavaBean類(lèi):MonthlySalesBean.java,代碼如下:
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;



public class MonthlySalesBean
{

private int employeeID;;
private String last = null;
private String first = null;
private BigDecimal total = null;
private List sales = null;
private LatestSale latestSale = null;

public static List createBeanCollection ()
{
List list = new ArrayList ();
MonthlySalesBean msb = new MonthlySalesBean ();
msb.setEmployeeID(1);
msb.setFirst("John");
msb.setLast("Doe");
msb.setTotal(new BigDecimal ("1600.50"));
LatestSale ls = new LatestSale ();
ls.setAmount(new BigDecimal ("32.21"));
msb.setLatestSale(ls);
list.add(msb);
return list;
}

public int getEmployeeID()
{
return employeeID;
}

public void setEmployeeID(int employeeID)
{
this.employeeID = employeeID;
}

public String getFirst()
{
return first;
}

public void setFirst(String first)
{
this.first = first;
}

public String getLast()
{
return last;
}

public void setLast(String last)
{
this.last = last;
}

public BigDecimal getTotal()
{
return total;
}

public void setTotal(BigDecimal total)
{
this.total = total;
}

public List getSales()
{
return sales;
}

public void setSales(List sales)
{
this.sales = sales;
}

public LatestSale getLatestSale()
{
return latestSale;
}

public void setLatestSale(LatestSale latestSale)
{
this.latestSale = latestSale;
}
}

將上面的類(lèi)打成一個(gè)jar包,并置于classpath目錄下,則iReport可以進(jìn)行訪問(wèn)。使用JavaBean做為數(shù)據(jù)源,為了在設(shè)計(jì)報(bào)表時(shí)能夠看到數(shù)據(jù),在程序中要為iReport提供一個(gè)靜態(tài)方法,該方法返回上面定義JavaBean的一個(gè)結(jié)果集,這個(gè)靜態(tài)方法可能在程序運(yùn)行中并不是必須的,但是在iReport中它確實(shí)必須的,換句話(huà)說(shuō),這個(gè)靜態(tài)方法是專(zhuān)門(mén)為iReport量身定做的,為了iReport在設(shè)計(jì)報(bào)表時(shí)能夠調(diào)用這個(gè)靜態(tài)方法返回相應(yīng)的JavaBean結(jié)果集,以便設(shè)計(jì)的報(bào)表放在Java項(xiàng)目中之前就能像使用SQL數(shù)據(jù)庫(kù)數(shù)據(jù)源一樣可以瀏覽。在iReport中先進(jìn)行數(shù)據(jù)源的連接配置,此處采用是JavaBeans set data source連接方式:

圖2.在iReport進(jìn)行數(shù)據(jù)源的連接
三、處理iBati返回?cái)?shù)據(jù)
如果iBATIS沒(méi)有采用JavaBean作為返回對(duì)象,則可以采用java.util.map作為數(shù)據(jù)的返回對(duì)象。采用java.util.Map對(duì)象,需要額外的一些步驟。下面的代碼則說(shuō)明了iBATIS的select語(yǔ)句返回的java.util.Map對(duì)象。Src/ iBATIS.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 1.0//EN"
"http://iBATIS.apache.org/dtd/sql-map-2.dtd">
<sqlMap>

<select id="salesByListOfMapsSQL" resultClass="java.util.HashMap">
SELECT
E.EMPLOYEE_ID "ID",
E.FIRST_NAME "FIRST",
E.LAST_NAME "LAST",
MS.TOTAL_SALES "TOTAL",
MS.LATEST_SALE
FROM
EMPLOYEE E,
MONTHLY_SALES MS
WHERE
E.EMPLOYEE_ID = MS.EMPLOYEE_ID
AND MS.MONTH = #value#
</select>
<resultMap id="searchResultList" class="MonthlySalesBean">
<result property="employeeID" column="ID"/>
<result property="first" column="FIRST"/>
<result property="last" column="LAST"/>
<result property="total" column="TOTAL"/>
<result property="latestSale.amount" column="LATEST_SALE"/>
</resultMap>
<select id="salesByJavaBeansSQL" resultMap="searchResultList">
SELECT
E.EMPLOYEE_ID "ID",
E.FIRST_NAME "FIRST",
E.LAST_NAME "LAST",
MS.TOTAL_SALES "TOTAL",
MS.LATEST_SALE
FROM
EMPLOYEE E,
MONTHLY_SALES MS
WHERE
E.EMPLOYEE_ID = MS.EMPLOYEE_ID
AND MS.MONTH = #value#
</select>
</sqlMap>

上面的代碼返回的對(duì)象即為map對(duì)象。請(qǐng)注意,map對(duì)象中的Key值直接來(lái)自于select語(yǔ)句,因此,像TO_CHAR(MS.TOTAL_SALES)這樣的表達(dá)式在報(bào)表中不提倡使用。因此,比較人性化的為字段命名,是一件很值得的事情。因?yàn)?/span>map的key值是作為java.lang.Object類(lèi)型來(lái)進(jìn)行存儲(chǔ)的,因此有必要對(duì)字段返回類(lèi)型進(jìn)行一下整理。
真正的數(shù)據(jù)填充類(lèi)應(yīng)該是ServiceLocatorBean.java類(lèi),其代碼如下所示:
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;

import javax.servlet.ServletContext;

import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;


public class ServiceLocatorBean implements ServiceLocatorIF
{
private static final long serialVersionUID = -7166271873610635886L;

//the Spring application context
private ApplicationContext appContext;
DAO dao = null;

public ServiceLocatorBean()
{

try
{
// get the spring context
ServletContext context = FacesUtils.getServletContext();
this.appContext = WebApplicationContextUtils.getRequiredWebApplicationContext(context);
// create instance of the business object
this.dao = (DAO) this.lookupService("dao");
Connection conn = this.dao.getSqlMapClient().getDataSource().getConnection();
conn.setAutoCommit(false);

/**//*
Creating a statement lets us issue commands against
the connection.
*/
Statement s = conn.createStatement();
// just in case old tables from prior run (after first run which
// will create the USER1 schema)

try
{
s.execute("drop table employee");
s.execute("drop table monthly_sales");

} catch (Exception ex)
{
// not to be concerned (at least in this example
}

/**//*
We create a table, add a few rows, and update one.
*/
s.execute("create table employee (employee_id int, first_name varchar(40), last_name varchar(40))");
s.execute("insert into employee values (1,'sterning', 'chen')");
s.execute("insert into employee values (2,'yuxuan', 'Wand')");
s.execute("insert into employee values (3,'Mickey', 'Li')");
s.execute("create table monthly_sales (employee_id int, total_sales numeric(16, 2), latest_sale numeric(8, 2), month int)");
s.execute("insert into monthly_sales values (1, 1600.50, 32.50, 1)");
s.execute("insert into monthly_sales values (2, 1544.20, 12.50, 1)");
s.execute("insert into monthly_sales values (3, 18814.80, 78.65, 1)");
s.execute("insert into monthly_sales values (1, 1450.50, 10.65, 2)");
s.execute("insert into monthly_sales values (2, 2004.25, 52.10, 2)");
s.execute("insert into monthly_sales values (3, 9819.00, 40.65, 2)");
s.close();
conn.commit();

} catch (SQLException sqle)
{
// just means the tables already exist
sqle.printStackTrace();

} catch (Exception ex)
{
ex.printStackTrace();
}
}

public DAO getDao()
{
return this.dao;
}

public Object lookupService(String serviceBeanName)
{
return appContext.getBean(serviceBeanName);
}

}

四、將iBATIS數(shù)據(jù)填入JasperReport中
就通常而言,采用Java Bean作為iBATIS的返回對(duì)象,相比起java.util.Map對(duì)象來(lái)說(shuō),更加的方便與可行。很多的開(kāi)發(fā)人員采用iBATIS的這種方式來(lái)進(jìn)行數(shù)據(jù)的映射,同時(shí),此方法還可以無(wú)縫的將iBATIS與JapserReport集成起來(lái)。
在JasperReport中,提供了一個(gè)JRDataSource的實(shí)現(xiàn),從而開(kāi)發(fā)人員可以通過(guò)此類(lèi)來(lái)傳遞iBATIS的list對(duì)象給JasperReport模板。而JRBeanCollectionDataSource類(lèi)使用JavaBean來(lái)構(gòu)造,從而可以通過(guò)循環(huán)查找collection并獲得相應(yīng)的bean屬性。如下的代碼示例說(shuō)明了如何在調(diào)用JasperReport引擎時(shí)實(shí)例化JRBeanCollectionDataSource對(duì)象。
import java.io.File;
import java.util.HashMap;
import java.util.List;

import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.engine.util.JRLoader;



public class SearchBean
{
private final static String JAVA_BEAN_REPORT = "monthly_sales_java_beans.jasper";
private final static String LIST_OF_MAP_REPORT = "monthly_sales_list_of_maps.jasper";


public String generateFromJavaBeans ()
{

try
{
ServiceLocatorIF sl = (ServiceLocatorIF) FacesUtils
.getManagedBean("serviceLocatorBean");
List list = sl.getDao().getSqlMapClient().queryForList("salesByJavaBeansSQL", month);
FacesUtils.setSessionAttribute("JASPER_PRINT", generateReport (list, JAVA_BEAN_REPORT));
viewReport = "true";

} catch (Exception ex)
{
ex.printStackTrace();
}
return null;
}

public String generateFromListOfMaps ()
{

try
{
ServiceLocatorIF sl = (ServiceLocatorIF) FacesUtils
.getManagedBean("serviceLocatorBean");
List list = sl.getDao().getSqlMapClient().queryForList("salesByListOfMapsSQL", month);
FacesUtils.setSessionAttribute("JASPER_PRINT", generateReport (list, LIST_OF_MAP_REPORT));
viewReport = "true";

} catch (Exception ex)
{
ex.printStackTrace();
}
return null;
}


private JasperPrint generateReport (List dataList, String reportName)
{
JasperPrint jasperPrint = null;

try
{
String localPath = FacesUtils.getServletContext().getRealPath("/");
File reportFile = new File(localPath + "WEB-INF" + File.separator + reportName);
if (!reportFile.exists())
throw new JRRuntimeException(".jasper file not found. The report design must be compiled first.");
JasperReport jasperReport = (JasperReport)JRLoader.loadObject(reportFile.getPath());

if (reportName.equals(JAVA_BEAN_REPORT))
{
jasperPrint = JasperFillManager.fillReport(
jasperReport,
new HashMap(),
new JRBeanCollectionDataSource (dataList));

} else
{
jasperPrint = JasperFillManager.fillReport(
jasperReport,
new HashMap(),
new CustomJRDS (dataList));
}

} catch (Exception ex)
{
ex.printStackTrace();
}
return jasperPrint;
}


public String getMonth()
{
return month;
}


public void setMonth(String month)
{
this.month = month;
}


public String getViewReport()
{
return viewReport;
}


public void setViewReport(String viewReport)
{
this.viewReport = viewReport;
}
private String month = null;
private String viewReport = null;
}

在上面的代碼中,定義的參數(shù)map,是在運(yùn)行時(shí)傳遞相關(guān)的參數(shù)值給JasperReport。例如,可以在報(bào)表模板中定義一個(gè)名為REPORT_TITLE的參數(shù),然后在運(yùn)行時(shí)傳遞這一參數(shù)的值給它,傳遞的方式一般是?。祵?duì)的形式。例如Key=REPORT_TITLE,Value=Sale Report。當(dāng)然,參數(shù)是傳遞給fillReport方法。然后,JasperReport會(huì)加載已經(jīng)編譯好的Jasper模板文件(.jasper)。最后調(diào)用靜態(tài)的fillReport方法。
而JasperPrint對(duì)象是在數(shù)據(jù)展示或顯示時(shí)需要用到的。而在本例中,采用了JRPdfExporter來(lái)作為輸出的格式,即輸出為PDF格式文件,請(qǐng)參考PdfServlet.java文件,代碼如下所示:
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRExporterParameter;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.export.JRPdfExporter;



public class PdfServlet extends HttpServlet
{

public void service(HttpServletRequest request, HttpServletResponse response)

throws IOException, ServletException
{

JasperPrint jasperPrint = (JasperPrint) request.getSession()
.getAttribute("JASPER_PRINT");

List jasperPrintList = new ArrayList();

jasperPrintList.add(jasperPrint);

JRPdfExporter exporter = new JRPdfExporter();
exporter.setParameter(JRExporterParameter.JASPER_PRINT_LIST,
jasperPrintList);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
exporter.setParameter(JRExporterParameter.OUTPUT_STREAM, baos);


try
{
exporter.exportReport();

} catch (JRException e)
{
throw new ServletException(e);
}

byte[] bytes = baos.toByteArray();


if (bytes != null && bytes.length > 0)
{
response.setContentType("application/pdf");
response.setContentLength(bytes.length);
ServletOutputStream ouputStream = response.getOutputStream();


try
{
ouputStream.write(bytes, 0, bytes.length);
ouputStream.flush();

} finally
{

if (ouputStream != null)
{

try
{
ouputStream.close();

} catch (IOException ex)
{
}
}
}
}
}
}

盡管上面的JasperReport機(jī)制可以將iBATIS連接起來(lái),但應(yīng)該根據(jù)項(xiàng)目報(bào)表的需要對(duì)JavaBean進(jìn)行修改與調(diào)整。而JasperReport字段對(duì)象可以很好的與普通的JDBC字段進(jìn)行匹配。例如,JasperReport將Oracle的numeric字段類(lèi)型對(duì)應(yīng)的轉(zhuǎn)成java.math.BigDecimal對(duì)象類(lèi)型。而在iBATIS的Bean屬性應(yīng)該與JasperReport中定義的字段類(lèi)型進(jìn)行很好的匹配。需要對(duì)字段的類(lèi)型進(jìn)行認(rèn)真仔細(xì)的選擇,因?yàn)椴煌?lèi)型或是不同表達(dá)式對(duì)數(shù)據(jù)的展示有不同的效果。例如,BigDecimal類(lèi)型比String類(lèi)型更加適合貨幣格式。
五、代碼運(yùn)行效果
1.系統(tǒng)主界面

圖3.報(bào)表運(yùn)行主界面
2.采用JavaBean生成報(bào)表

圖4.采用JavaBean生成報(bào)表
六、小結(jié)
在本文中,筆者展示了如何使用比較成熟的iBATIS數(shù)據(jù)框架來(lái)對(duì)JasperReport進(jìn)行數(shù)據(jù)填充。iBATIS最大的特點(diǎn)是簡(jiǎn)單,而iBATIS所擁有的易維護(hù)及易配置特性,在JasperReport中充分的體現(xiàn)出來(lái)了。這種簡(jiǎn)單與靈活性,正好彌補(bǔ)了JasperReport在這方面的不足,從而達(dá)到靈活開(kāi)發(fā)Web報(bào)表的目的。