使用Javabean作为数据源的JasperReport报表(通过WebService/RMI调用数据)
-
博客分类:
- 探索实践
之前我们用JasperReport制作报表使用的是JDBC数据源。当使用JDBC作为数据源时,JasperReport在运行时会检测数据库是否可用,也就是说它会自己连接一下数据库。而我们要使用远程方法时,数据库不在本地,那么我们就不能使用JDBC数据源了,因为JasperReport在不可访问数据源时会抛异常。此时,我们就必须使用JavaBean作为数据源了。比如通过WebService/RMI方式获取数据的话,就要走JavaBean数据源的方式了。
先定义一个功能需求,然后我们展开说明。一个车辆行驶数据分析系统中需要实时获取行驶数据,这需要车载设备来实现。数据上传存储在Web端的数据库中,我们需要从数据库中获取数据来显示内容。
使用JavaBean作为数据源,首先我们要扩展JasperReport的数据源。实现方法很简单,我们定义一个类去实现net.sf.jasperreports.engine.JRDataSource接口即可,覆盖其中的方法。下面给出一个示例。
- package xxx.xxx.web.bean;
- import java.util.List;
- import net.sf.jasperreports.engine.JRDataSource;
- import net.sf.jasperreports.engine.JRException;
- import net.sf.jasperreports.engine.JRField;
- public class DrivingHistoryDataSource implements JRDataSource {
- //数据集合,使用到了一个VO值对象
- private List<DrivingHistoryDataVO> list;
- private int i = -1;
- //构造方法,初始化list
- public DrivingHistoryDataSource(List<DrivingHistoryDataVO> list) {
- super();
- this.list = list;
- }
- //覆盖接口中的方法,给List添加参数
- public Object getFieldValue(JRField jrField) throws JRException {
- Object value = null;
- String fieldName = jrField.getName();
- if ("id".equals(fieldName)) {
- value = list.get(i).getId();
- } else if (){
- //剩余参数的填充这里就省略了,Java 7中的Switch可以使用字符串了就不用这样else if重复写了。
- }
- return value;
- }
- //覆盖接口中的next()方法
- public boolean next() throws JRException {
- i++;
- if (list == null) {
- return false;
- }
- return (i < list.size());
- }
- }
package xxx.xxx.web.bean;
import java.util.List;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
public class DrivingHistoryDataSource implements JRDataSource {
//数据集合,使用到了一个VO值对象
private List<DrivingHistoryDataVO> list;
private int i = -1;
//构造方法,初始化list
public DrivingHistoryDataSource(List<DrivingHistoryDataVO> list) {
super();
this.list = list;
}
//覆盖接口中的方法,给List添加参数
public Object getFieldValue(JRField jrField) throws JRException {
Object value = null;
String fieldName = jrField.getName();
if ("id".equals(fieldName)) {
value = list.get(i).getId();
} else if (){
//剩余参数的填充这里就省略了,Java 7中的Switch可以使用字符串了就不用这样else if重复写了。
}
return value;
}
//覆盖接口中的next()方法
public boolean next() throws JRException {
i++;
if (list == null) {
return false;
}
return (i < list.size());
}
}
因为扩展的数据源中使用了VO值对象,这里给出这个值对象。使用VO的好处就是从数据库读取的结果集直接封装成自定义的VO类型,放在List中也是泛型的一种实现,非常方便使用。
- package xxx.xxx.web.bean;
- import java.io.Serializable;
- public class DrivingHistoryDataVO implements Serializable{
- private int id;//主键
- //其余的属性省略了
- //默认的构造方法
- public DrivingHistoryDataVO() {
- super();
- }
- //带参数的构造方法,省略其余参数
- public DrivingHistoryDataVO(int id) {
- super();
- this.id = id;
- }
- public int getId() {
- return id;
- }
- public void setId(int id) {
- this.id = id;
- }
- //其余的getter和setter方法省略了
- }
package xxx.xxx.web.bean;
import java.io.Serializable;
public class DrivingHistoryDataVO implements Serializable{
private int id;//主键
//其余的属性省略了
//默认的构造方法
public DrivingHistoryDataVO() {
super();
}
//带参数的构造方法,省略其余参数
public DrivingHistoryDataVO(int id) {
super();
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
//其余的getter和setter方法省略了
}
数据源我们都配置好了,剩下的就是往JasperReport模板文件中填充了,而模板文件的设计是和普通JDBC数据源不同的,这里我们来详细说明一下。iReport工具我们使用3.7.2版本的,同时项目类库中JasperReport的版本也要和iReport工具一致,否则是不能编译的。
打开iReport,点击工具中的选项,找到classpath选项卡,增加我们的项目的类路径,这样我们就能在iReport中使用我们设定好的VO对象了。如下图所示:
设置好classpath后,我们开始设计报表模板,首先要设置数据源,我们选择JavaBean数据源,这里的Class Name要输入类的全名(含包名),然后点击Read attributes就可以读取到类中的属性了,我们选择全部添加进来,如下图所示:
我们得到如下的设计模板。
这里我们说几个注意的地方。日期字段可能是多条相同的记录,那么相同的日期只显示一次,在模板中我们需要多设置一下,就是把属性中的Print Report Value选项去掉,那么为了显示的好看,我们还需要特殊设置一下Padding and Borders,下边界不能显示。如下图所示:
而问题还没有全部解决,因为重复不显示的那部分边界就也不显示了,所以还需要特殊处理重复不显示那部分格子的边界。我们添加一个静态文本块static field,设置和日期字段完全重合。如下图所示:
还要说明的是数据超长问题的处理,正如这个模板中的起始地点和结束地点,因为显示的数据过多而留给他们的空间有限,如果这两个字段的数据过长,那么显示就会出现问题,这是必须要处理的问题。这个处理也很简单,我们选中需要超长处理的字段,然后在属性面板中勾选Stretch With Overflow选项,这时如果数据超长就会自动换行了。
而问题又出现了,如果这两个字段自动换行了,那么必然把一行上的其他元素都给撑大了,那么还需要对其他元素进行设置。选中其他的所有元素,在属性面板的Stretch Type中选择Relative to Tallest Object就可以自适应到最高换行的元素了,显示效果就正常了。
若报表在页面显示时,也要考虑日期合并的问题,那么就需要在流程中进行处理了。而页面中的合并涉及到rowspan的数值和td中的参数,这是需要处理的地方。这里很自然想到设计一个VO来匹配页面。那么我们设计ColsVO来实现,代码如下:
- package xxx.xxx.web.bean;
- /**
- * 统计报表用VO,代表一个单元格
- */
- public class ColsVO {
- private String value;//单元格显示的值
- private int rowspan;//单元格占几行
- public ColsVO() {
- super();
- }
- public ColsVO(String value, int rowspan) {
- super();
- this.value = value;
- this.rowspan = rowspan;
- }
- public String getValue() {
- return value;
- }
- public void setValue(String value) {
- this.value = value;
- }
- public int getRowspan() {
- return rowspan;
- }
- public void setRowspan(int rowspan) {
- this.rowspan = rowspan;
- }
- }
package xxx.xxx.web.bean;
/**
* 统计报表用VO,代表一个单元格
*/
public class ColsVO {
private String value;//单元格显示的值
private int rowspan;//单元格占几行
public ColsVO() {
super();
}
public ColsVO(String value, int rowspan) {
super();
this.value = value;
this.rowspan = rowspan;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public int getRowspan() {
return rowspan;
}
public void setRowspan(int rowspan) {
this.rowspan = rowspan;
}
}
下面就是对取出的数据集合进行处理了,我们先来说明页面显示的代码的处理算法。
- public List<Map> getResultList(List<DrivingHistoryDataVO> dataList) {
- List<Map> returnList = new ArrayList<Map>();
- // 保证记录顺序不变,使用TreeMap
- Map<String, Object> tempMap = null;
- // 日期合并记录数
- int dateRowspan = 0;
- for (int i = 0; i < dataList.size(); i++) {
- tempMap = new HashMap<String, Object>();
- DrivingHistoryDataVO rawVO = dataList.get(i);
- // 先填充不需要合并的字段
- returnList.add(tempMap);
- // 需要合并日期,单独处理
- // 是否是最后一条记录的开关
- boolean last = (i == dataList.size() - 1);
- // 取出两条记录进行比较
- DrivingHistoryDataVO hdVo1 = null;
- DrivingHistoryDataVO hdVo2 = null;
- if (!last) {
- hdVo1 = dataList.get(i);
- hdVo2 = dataList.get(i + 1);
- } else {
- // 防止最后一条记录无法加入集合
- hdVo1 = dataList.get(i);
- if (dataList.size() != 1)
- hdVo2 = dataList.get(i - 1);
- else
- hdVo2 = dataList.get(i);
- }
- String date1 = hdVo1.getDate();
- String date2 = hdVo2.getDate();
- // 比较date1和date2,如果相同,计数器加1;不同,更新统一日期的第一条记录,计数器清零
- if (date1.equals(date2)) {
- if (dataList.size() == 1) {
- ColsVO colsVO = new ColsVO(hdVo1.getDate(), 1);
- returnList.get(0).put("colsVO", colsVO);
- } else if (last) {
- ColsVO colsVO = new ColsVO(hdVo1.getDate(), dateRowspan + 1);
- returnList.get(i - dateRowspan).put("colsVO", colsVO);
- } else {
- dateRowspan++;
- }
- } else {
- ColsVO colsVO = new ColsVO(hdVo1.getDate(), dateRowspan + 1);
- returnList.get(i - dateRowspan).put("colsVO", colsVO);
- dateRowspan = 0;
- }
- }
- return returnList;
- }
public List<Map> getResultList(List<DrivingHistoryDataVO> dataList) {
List<Map> returnList = new ArrayList<Map>();
// 保证记录顺序不变,使用TreeMap
Map<String, Object> tempMap = null;
// 日期合并记录数
int dateRowspan = 0;
for (int i = 0; i < dataList.size(); i++) {
tempMap = new HashMap<String, Object>();
DrivingHistoryDataVO rawVO = dataList.get(i);
// 先填充不需要合并的字段
returnList.add(tempMap);
// 需要合并日期,单独处理
// 是否是最后一条记录的开关
boolean last = (i == dataList.size() - 1);
// 取出两条记录进行比较
DrivingHistoryDataVO hdVo1 = null;
DrivingHistoryDataVO hdVo2 = null;
if (!last) {
hdVo1 = dataList.get(i);
hdVo2 = dataList.get(i + 1);
} else {
// 防止最后一条记录无法加入集合
hdVo1 = dataList.get(i);
if (dataList.size() != 1)
hdVo2 = dataList.get(i - 1);
else
hdVo2 = dataList.get(i);
}
String date1 = hdVo1.getDate();
String date2 = hdVo2.getDate();
// 比较date1和date2,如果相同,计数器加1;不同,更新统一日期的第一条记录,计数器清零
if (date1.equals(date2)) {
if (dataList.size() == 1) {
ColsVO colsVO = new ColsVO(hdVo1.getDate(), 1);
returnList.get(0).put("colsVO", colsVO);
} else if (last) {
ColsVO colsVO = new ColsVO(hdVo1.getDate(), dateRowspan + 1);
returnList.get(i - dateRowspan).put("colsVO", colsVO);
} else {
dateRowspan++;
}
} else {
ColsVO colsVO = new ColsVO(hdVo1.getDate(), dateRowspan + 1);
returnList.get(i - dateRowspan).put("colsVO", colsVO);
dateRowspan = 0;
}
}
return returnList;
}
这样我们对原来的数据集合dataList进行遍历处理,就得到了处理后的集合returnList,在页面用FreeMarker进行遍历显示,代码如下:
- <#list returnList as info>
- <tr>
- <#if info.colsVO?has_content>
- <td rowspan="${ info.colsVO.rowspan}">${ info.colsVO.value}</td>
- <#else>
- </#if>
- <td>${info.XXX}</td>
- <td>${info.XXX}</td>
- </tr>
- </#list>
<#list returnList as info>
<tr>
<#if info.colsVO?has_content>
<td rowspan="${ info.colsVO.rowspan}">${ info.colsVO.value}</td>
<#else>
</#if>
<td>${info.XXX}</td>
<td>${info.XXX}</td>
</tr>
</#list>
这样就可以显示合并的效果了,我们看一下效果图片
日期字段就已经合并显示了,而直接遍历List是不能做到的,而合并算法可能存在问题,希望和大家交流。
剩下就是报表的打印了。看一下报表打印的核心代码。
- String jrxmlPath = this.getClass().getClassLoader().getResource("/xxx.xxx.template").getPath()+"/report.jrxml";
- JasperReport report = JasperCompileManager.compileReport(jrxmlPath);
- JasperPrint jasperPrint = JasperFillManager.fillReport(report,
- null, new DrivingHistoryDataSource(dataList));
String jrxmlPath = this.getClass().getClassLoader().getResource("/xxx.xxx.template").getPath()+"/report.jrxml";
JasperReport report = JasperCompileManager.compileReport(jrxmlPath);
JasperPrint jasperPrint = JasperFillManager.fillReport(report,
null, new DrivingHistoryDataSource(dataList));
其余部分就和正常是一样的了,只是数据源的设置上使用我们扩展的JRDataSource就行了。我们看看打印出来的PDF效果。
字段已经合并了,但是合并字段的居中还是个问题,需要继续研究。
前面说过使用JavaBean作为数据源是因为Web应用中数据是远程方法调用过来的,而不是在本地数据库生成的。下面用RMI方法来说明本例中的数据获取设置。当然WebService的实现还有很多。
- package xxx.xxx.service;
- import org.springframework.remoting.rmi.RmiProxyFactoryBean;
- /**
- * 获取RMI远程访问接口
- */
- public class WebServiceClientService{
- private String serviceName;//WebService服务名
- private String registryPort;//WebService注册端口号
- public void setServiceName(String serviceName) {
- this.serviceName = serviceName;
- }
- public void setRegistryPort(String registryPort) {
- this.registryPort = registryPort;
- }
- /**
- * 获取RMI接口
- * @param ip
- * @return
- */
- private RmiProxyFactoryBean getRmiProxyFactoryBean(String ip){
- RmiProxyFactoryBean rpf=new RmiProxyFactoryBean();
- StringBuffer url=new StringBuffer();
- url.append("rmi://");
- url.append(ip);
- url.append(":");
- url.append(registryPort);
- url.append("/");
- url.append(serviceName);
- rpf.setServiceUrl(url.toString());
- return rpf;
- }
- /**
- * 设置RMI远程访问接口生效
- */
- private void setRmiProxyFactoryBean(RmiProxyFactoryBean rpf){
- rpf.afterPropertiesSet();
- }
- /**
- * 获取RMI车辆数据接口
- */
- public DrivingHistoryDataWebService getDrivingHistoryDataWebService(String ip){
- RmiProxyFactoryBean rpf=this.getRmiProxyFactoryBean(ip);
- rpf.setServiceInterface(DrivingHistoryDataWebServiceIF.class);
- this.setRmiProxyFactoryBean(rpf);
- return (DrivingHistoryDataWebService)rpf.getObject();
- }
- }
package xxx.xxx.service;
import org.springframework.remoting.rmi.RmiProxyFactoryBean;
/**
* 获取RMI远程访问接口
*/
public class WebServiceClientService{
private String serviceName;//WebService服务名
private String registryPort;//WebService注册端口号
public void setServiceName(String serviceName) {
this.serviceName = serviceName;
}
public void setRegistryPort(String registryPort) {
this.registryPort = registryPort;
}
/**
* 获取RMI接口
* @param ip
* @return
*/
private RmiProxyFactoryBean getRmiProxyFactoryBean(String ip){
RmiProxyFactoryBean rpf=new RmiProxyFactoryBean();
StringBuffer url=new StringBuffer();
url.append("rmi://");
url.append(ip);
url.append(":");
url.append(registryPort);
url.append("/");
url.append(serviceName);
rpf.setServiceUrl(url.toString());
return rpf;
}
/**
* 设置RMI远程访问接口生效
*/
private void setRmiProxyFactoryBean(RmiProxyFactoryBean rpf){
rpf.afterPropertiesSet();
}
/**
* 获取RMI车辆数据接口
*/
public DrivingHistoryDataWebService getDrivingHistoryDataWebService(String ip){
RmiProxyFactoryBean rpf=this.getRmiProxyFactoryBean(ip);
rpf.setServiceInterface(DrivingHistoryDataWebServiceIF.class);
this.setRmiProxyFactoryBean(rpf);
return (DrivingHistoryDataWebService)rpf.getObject();
}
}
在Spring的配置文件中,要注入属性,代码如下:
- <bean id="drivingHistoryDataClientService" class="xxx.xxx.service.WebServiceClientService">
- <property name="serviceName" value="DrivingHistoryDataWebService"/>
- <property name="registryPort" value="1234"/>
- </bean>
<bean id="drivingHistoryDataClientService" class="xxx.xxx.service.WebServiceClientService">
<property name="serviceName" value="DrivingHistoryDataWebService"/>
<property name="registryPort" value="1234"/>
</bean>
我们在Action代码中就可以通过RMI的接口获取远端的数据了。代码如下:
- DrivingHistoryDataWebService hdws = getWebServiceClientService().getDrivingHistoryDataWebService(ip);
- List<DrivingHistoryDataVO> dataList = hdws.getDrivingHistoryDataList(new Object[]{xxx,xxx,xxx});
DrivingHistoryDataWebService hdws = getWebServiceClientService().getDrivingHistoryDataWebService(ip);
List<DrivingHistoryDataVO> dataList = hdws.getDrivingHistoryDataList(new Object[]{xxx,xxx,xxx});
这样就可以通过RMI获取到远端的实现数据了,远端的实现这里就不列出了。
一家之言,仅供参考,希望对使用者有用,欢迎交流。
http://sarin.iteye.com/blog/693622