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

圖1.所使用的jar包
二、在iReport中可視化定制模板
定制報表格式有二種方式,一種就是寫jrxml文件,其實就是xml文件,只不過是后綴名不一樣罷了。另一種方式更直接,就是生成一個JasperDesign類的實例,在japsperDesign中自己定義模板。jrxml文件也是通過一個JRXmlLoad加載過來,轉成JasperDesign類的實例。也就是說寫jrxml文件還需要進行解析,加載。現實中我們使用的報表一般格式比較固定,因而可以通過先使用iReport工具生成模板,再加載解析的方式。這種方式簡單,而且可見性強。
iReport做為一個優秀的報表設計器,有著功能非常強大的特性。作為開源的Java程序,不但有適合于Windows安裝的應用程序,同時,還提供完全開放的源代碼,可供參考及原理分析。在本文中,主要通過圖形界面中的模板設計,以及與數據庫的連接等一系列的操作,來介紹如何定制一定要求的報表模板。
通過iReport可初見化的圖形界面,可以設計出各種各樣的簡單或復雜的報表。通過iReport的這種可視化界面設計,可以為JasperReport提供優秀的報表模板,而無須去理解或是掌握那些復雜的XML語法。如此則可以Web報表開發節省大量的開發時間。
在進行iReport模板設計之前,需要編寫JavaBean類: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;
}
}

將上面的類打成一個jar包,并置于classpath目錄下,則iReport可以進行訪問。使用JavaBean做為數據源,為了在設計報表時能夠看到數據,在程序中要為iReport提供一個靜態方法,該方法返回上面定義JavaBean的一個結果集,這個靜態方法可能在程序運行中并不是必須的,但是在iReport中它確實必須的,換句話說,這個靜態方法是專門為iReport量身定做的,為了iReport在設計報表時能夠調用這個靜態方法返回相應的JavaBean結果集,以便設計的報表放在Java項目中之前就能像使用SQL數據庫數據源一樣可以瀏覽。在iReport中先進行數據源的連接配置,此處采用是JavaBeans set data source連接方式:

圖2.在iReport進行數據源的連接
三、處理iBati返回數據
如果iBATIS沒有采用JavaBean作為返回對象,則可以采用java.util.map作為數據的返回對象。采用java.util.Map對象,需要額外的一些步驟。下面的代碼則說明了iBATIS的select語句返回的java.util.Map對象。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>

上面的代碼返回的對象即為map對象。請注意,map對象中的Key值直接來自于select語句,因此,像TO_CHAR(MS.TOTAL_SALES)這樣的表達式在報表中不提倡使用。因此,比較人性化的為字段命名,是一件很值得的事情。因為map的key值是作為java.lang.Object類型來進行存儲的,因此有必要對字段返回類型進行一下整理。
真正的數據填充類應該是ServiceLocatorBean.java類,其代碼如下所示:
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數據填入JasperReport中
就通常而言,采用Java Bean作為iBATIS的返回對象,相比起java.util.Map對象來說,更加的方便與可行。很多的開發人員采用iBATIS的這種方式來進行數據的映射,同時,此方法還可以無縫的將iBATIS與JapserReport集成起來。
在JasperReport中,提供了一個JRDataSource的實現,從而開發人員可以通過此類來傳遞iBATIS的list對象給JasperReport模板。而JRBeanCollectionDataSource類使用JavaBean來構造,從而可以通過循環查找collection并獲得相應的bean屬性。如下的代碼示例說明了如何在調用JasperReport引擎時實例化JRBeanCollectionDataSource對象。
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;
}

在上面的代碼中,定義的參數map,是在運行時傳遞相關的參數值給JasperReport。例如,可以在報表模板中定義一個名為REPORT_TITLE的參數,然后在運行時傳遞這一參數的值給它,傳遞的方式一般是健/值對的形式。例如Key=REPORT_TITLE,Value=Sale Report。當然,參數是傳遞給fillReport方法。然后,JasperReport會加載已經編譯好的Jasper模板文件(.jasper)。最后調用靜態的fillReport方法。
而JasperPrint對象是在數據展示或顯示時需要用到的。而在本例中,采用了JRPdfExporter來作為輸出的格式,即輸出為PDF格式文件,請參考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機制可以將iBATIS連接起來,但應該根據項目報表的需要對JavaBean進行修改與調整。而JasperReport字段對象可以很好的與普通的JDBC字段進行匹配。例如,JasperReport將Oracle的numeric字段類型對應的轉成java.math.BigDecimal對象類型。而在iBATIS的Bean屬性應該與JasperReport中定義的字段類型進行很好的匹配。需要對字段的類型進行認真仔細的選擇,因為不同類型或是不同表達式對數據的展示有不同的效果。例如,BigDecimal類型比String類型更加適合貨幣格式。
五、代碼運行效果
1.系統主界面

圖3.報表運行主界面
2.采用JavaBean生成報表

圖4.采用JavaBean生成報表
六、小結
在本文中,筆者展示了如何使用比較成熟的iBATIS數據框架來對JasperReport進行數據填充。iBATIS最大的特點是簡單,而iBATIS所擁有的易維護及易配置特性,在JasperReport中充分的體現出來了。這種簡單與靈活性,正好彌補了JasperReport在這方面的不足,從而達到靈活開發Web報表的目的。