最近在整理项目上的报表功能,因为项目比较久远,留存的文档也少,因此重新部署过程中也遇到了不少的坑,记录下来方便后来者。项目中用的是Birt来自定义报表,当时选择Birt的原因主要是因为产品的需要,为了满足用户个性化报表的需求,而且以最小的开发量来快速的定制化报表,预览报表以及生成报表文件等功能。但由于Birt也是一个比较老的技术,熟悉.net的朋友应该都有过水晶报表的开发经验,Birt很类似于水晶报表。Birt现在的更新停留在2016年,因此这几年也没有任何的更新,会存在一些问题:
- JDK版本的支持,JDK的版本目前测试只能到1.8,而且其他类型的JDK,比如corretto JDK 1.8有些功能不能正常运行
- 由于很多库的版本比较低,一些涉及到安全的库升级比较困难。
- 另外它是基于eclipse插件开发,因此暂时在IntellJ上还不支持。
- 当前的很多实例都是基于JAVA EE开发,新的SpringBoot实例比较少。因此,本文在总计Birt的基本使用的基础上,将使用一个实际的实例来详细演示如何在SpringBoot上集成Birt,并提供报表的在线和离线生成功能。
Birt介绍
BIRT是一个Eclipse-based开放源代码的报表系统,它主要是用在基于Java和J2ee的web应用程序上。BIRT主要由两部分组成:一个是基于Eclipse的报表设计器和一个可以添加到应用服务器的运行组建。BIRT同时提供一个图形报表制作引擎。BIRT可以生成图片、导出Excel、pdf等。
基本概念
1)数据源,数据的提供者。如xml数据源、jdbc数据源等。也支持Hive和Cassandra等大数据源。也可以支持脚本数据源。
2)数据集,数据集合,即查询的结果。
3)报表以及报表项,报表可视为是一组数据集的表现形式,而报表项是具体形式表现的单元。
4)报表参数,用于查询报表的查询参数5)模板和库,主要用于复用报表,提高报表开发的效率。
报表类型
列表
列表是最简单的报表。当列表变长时,你可以把相关数据增加到同一分组。如果数据是数字类型的,你可以添加到“总数”、“平均”、或其他汇总中。
图表
当需要图表表现时,数字型数据比较好理解。BIRT 也提供饼状、线状以及柱状图标等。
交叉表
交叉表(也叫做十字表格或矩阵)用两种维度展示数据。
信函和文档
通知、信件、以及其他文本文档都很容易通过 BIRT 方便建立。文档包括正文、格式、列表、图表等。
混合报表
可以组合各种类型的报表到一个报表上。
报表设计器
包括布局视图(Layout),属性编辑器(Property Editor),报表预览(Preview),代码编辑器(Script)等。
整体。如下图:
准备环境
下载download birt开发工具 http://download.eclipse.org/birt/downloads 选All in one,安装后打开eclipse.exe或者自行下载eclipe安装,然后安装Birt runtime和design tool。
报表设计
第一步,创建报表
File->new->New Report , 新建立一个report,命名为book.rptdesign,放在resouces/report目录下
第二步,创建数据源
在Outline里找到Data source,新建DataSource,在下拉框里选Scripted Data Source,Scripted Data Source是指通过script来获取到数据源。
第三步,创建dataset
在Outline里找到刚刚建立Data set,双击找到DataSource,关联上刚刚建立的DataSource。Data set 是指从DataSource来组装出需要的数据集合。
第四步,设置dataset输出列
在Outline里找到Data set,新建DataSet,在OutPut columns填入Year type选string,name type选String,author type选String。
第五步,设置报表参数
在Outline里找到Report Parameters,新建立一个参数为year,Data type为String,Display type为Text Box。
第六步,设置body
在Outline里找到Body,在Outline里找到Rport Items里,拖动一个Table到右侧的layout里,会自动弹出对话框,DataSet选刚才的建立的DataSet,选中binding columns。并输入表头并绑定cell到dataset的输出参数。
第七步,预览报表
预览report,可以在birt里选window->preference->web browser,勾上use external web browser ,在下面的选项框里选你的浏览器。然后在工具栏里选择播放标志的下拉选view report as html或其它的选项。预览时,会提示输入年份,输入年份后,就可以看到报表的雏形。
SpringBoot服务开发
使用SpringBoot开发后端服务,来为报表数据提供数据源。下面以一个简单的实例来演示如果生成报表。
引入依赖包
<dependency>
<groupId>org.eclipse.birt.runtime</groupId>
<artifactId>org.eclipse.birt.runtime</artifactId>
<version>4.4.2</version>
<exclusions>
<exclusion>
<groupId>org.eclipse.birt.runtime</groupId>
<artifactId>org.eclipse.orbit.mongodb</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.birt.runtime</groupId>
<artifactId>viewservlets</artifactId>
<version>4.4.2</version>
</dependency>
<dependency>
<groupId>org.apache.axis</groupId>
<artifactId>axis</artifactId>
<version>1.4</version>
</dependency>
注意:org.eclipse.birt.runtime:org.eclipse.orbit.mongodb排除掉,不然会启动报错
如果需要预览报表,需要引入更多的包,axis,commons-discovery,
viewservlets,jaxrpc-api
BirtEngineFactory
public class BirtEngineFactory implements FactoryBean<IReportEngine>, ApplicationContextAware, DisposableBean {
public boolean isSingleton(){ return true ; }
private ApplicationContext context ;
private IReportEngine birtEngine ;
private File _resolvedDirectory ;
private java.util.logging.Level logLevel ;
public void setApplicationContext(ApplicationContext ctx){
this.context = ctx;
}
public void destroy() throws Exception {
birtEngine.destroy();
Platform.shutdown() ;
}
public void setLogLevel( java.util.logging.Level ll){
this.logLevel = ll ;
}
public void setLogDirectory( org.springframework.core.io.Resource resource ){
File f=null;
try {
f = resource.getFile();
validateLogDirectory(f);
this._resolvedDirectory = f ;
} catch (IOException e) {
throw new RuntimeException("�couldn'�t set the log directory�");
}
}
private void validateLogDirectory (File f) {
Assert.notNull ( f , " the directory must not be null");
Assert.isTrue(f.isDirectory() , " the path given must be a directory");
Assert.isTrue(f.exists() , "the path specified must exist!");
}
public void setLogDirectory ( File f ){
validateLogDirectory(f) ;
this._resolvedDirectory = f;
}
@SuppressWarnings("unchecked")
public IReportEngine getObject(){
EngineConfig config = new EngineConfig();
//This line injects the Spring Context into the BIRT Context
config.getAppContext().put("spring", this.context );
config.setLogConfig( null != this._resolvedDirectory ? this._resolvedDirectory.getAbsolutePath() : null , this.logLevel);
try {
Platform.startup( config );
}
catch ( BirtException e ) {
throw new RuntimeException ( "Could not start the Birt engine!", e) ;
}
IReportEngineFactory factory = (IReportEngineFactory) Platform.createFactoryObject( IReportEngineFactory.EXTENSION_REPORT_ENGINE_FACTORY );
IReportEngine be = factory.createReportEngine( config );
this.birtEngine = be ;
return be ;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public Class getObjectType() {
return IReportEngine.class;
}
}
这个类也实现了DisposableBean接口确保当应用程序关闭时报表引擎也能关闭,destroy方法将被调用。此类也实现了ApplicationContextAware接口来获取Spring ApplicationContext实例,存储ApplicationContext并传递到报表引擎。使用下面代码段
config.getAppContext().put("spring", this.context );
在报表里就可以使用下面的语句来获取ApplicationContext,进而获取bean实例。
spring = reportContext.getAppContext().get("spring");
var bookService = spring.getBean("bookService");
BirtConfiguration
@Configuration
public class BirtConfiguration {
@Value("${birt.log.location}")
String logLocation;
//产生FactoryBean<IReportEngine>
//在使用的依赖IReportEngine注入时候会调用到
@Bean
protected BirtEngineFactory engine(){
BirtEngineFactory factory = new BirtEngineFactory() ;
//Enable BIRT Engine Logging
factory.setLogLevel( Level.INFO);
factory.setLogDirectory( new FileSystemResource(logLocation));
return factory ;
}
}
BirtController
@RestController
public class ReportController {
@Autowired
BirtReportGenerator birtReportGenerator;
Logger logger = LoggerFactory.getLogger(ReportController.class);
@PostMapping("/report/book/{searchBy}/{seachValue}")
public String searchCarReport(@PathVariable("searchBy") String searchBy,
@PathVariable("seachValue") String searchValue) {
ReportParameter rm = new ReportParameter("book", "PDF");
rm.setParameter(searchBy, searchValue);
try {
ByteArrayOutputStream baos = birtReportGenerator.generate(rm);
FileOutputStream fops = new FileOutputStream("c:/report/bookreport_" + System.currentTimeMillis() + ".pdf");
fops.write(baos.toByteArray());
fops.close();
baos.close();
} catch (Exception e) {
logger.error("Error: " + e.getMessage());
}
return "success";
}
}
BirtGenerator
@Component
public class BirtReportGenerator {
@Autowired
private IReportEngine birtEngine;
public ByteArrayOutputStream generate(ReportParameter rptParam) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
IReportRunnable runnable = null;
ClassPathResource cpr = new ClassPathResource("report/" + rptParam.getReportName() + ".rptdesign");
runnable = birtEngine.openReportDesign(cpr.getInputStream());
IRunAndRenderTask runAndRenderTask = birtEngine.createRunAndRenderTask(runnable);
runAndRenderTask.setParameterValues(setParameters(runnable, rptParam.getParameter()));
IRenderOption options = new RenderOption();
if (rptParam.getFormat().equalsIgnoreCase("pdf")) {
PDFRenderOption pdfOptions = new PDFRenderOption(options);
pdfOptions.setOutputFormat("pdf");
pdfOptions.setOption(IPDFRenderOption.PAGE_OVERFLOW, IPDFRenderOption.FIT_TO_PAGE_SIZE);
pdfOptions.setOutputStream(baos);
runAndRenderTask.setRenderOption(pdfOptions);
}
runAndRenderTask.getAppContext().put(EngineConstants.APPCONTEXT_CLASSLOADER_KEY,
this.getClass().getClassLoader());
runAndRenderTask.run();
runAndRenderTask.close();
return baos;
}
protected HashMap<String, Object> setParameters(IReportRunnable report, Map<String, Object> m) throws Exception {
HashMap<String, Object> parms = new HashMap<String, Object>();
IGetParameterDefinitionTask task = birtEngine.createGetParameterDefinitionTask(report);
// 拿到birt里所有的parameter定义
Collection<IParameterDefnBase> params = task.getParameterDefns(true);
Iterator<IParameterDefnBase> iter = params.iterator();
while (iter.hasNext()) {
IParameterDefnBase param = (IParameterDefnBase) iter.next();
Object val = m.get(param.getName());
// 如果拿到birt的parameter有定义
if (val != null) {
parms.put(param.getName(), val);
}
}
task.close();
return parms;
}
}
bean service
@Service
public class BookService {
static List<Book> books;
static{
Book book1 = new Book();
book1.setYear("2000");
book1.setAuthor("谭浩强");
book1.setName("C++程序设计");
Book book2 = new Book();
book2.setYear("2005");
book2.setAuthor("[美] Bruce Eckel");
book2.setName("JAVA编程思想");
Book book3 = new Book();
book3.setYear("2002");
book3.setAuthor("严蔚敏");
book3.setName("数据结构");
books = Arrays.asList( book1, book2, book3 ) ;
}
public List getAllBooks (){
return this.books ;
}
public List getBooksByYear(String year){
List list=books.stream().filter(book->
book.getYear().equals(year)
).collect(Collectors.toList());
return list;
}
}
Report脚本开发
Birt脚本定义在Report Object, Report Elements, and Data Source(Sets)这三种对象上。不同阶段有不同的事件,如下图,
initialize脚本
在Birt里打开book.rptdesign,在Outline里找到Scripts,点book.rptdesign,然后在右边的窗口里切换到script,在左上方的script下来里找达到initialize,然后输入以下代码
spring = reportContext.getAppContext().get("spring");
var bookService = spring.getBean("bookService");
open脚本
在Birt里打开book.rptdesign,在Outline里找到dataset,然后在右边的窗口里切换到script,在左上的script下来里找到open,然后输入以下代码
var bookService = spring.getBean("bookService");
listdata = bookService.getBooksByYear(params["year"]);
count = 0;
fetch脚本
在Birt里打开book.rptdesign,在Outline里找到dataset,然后在右边的窗口里切换到script,在左上的script下来里找到fetch,然后输入以下代码
if(listdata.size() > count){
book = listdata.get(count);
row.year = book.getYear();
row.name = book.getName();
row.author = book.getAuthor();
count++;
return true;
}
return false;
运行报表
eclipse运行此报表程序,或者使用java -jar来运行此报表程序。打开postman,选择post请求,输入http://127.0.0.1:8080/report/book/year/2005,在c:/report下面将生成一个pdf文件。
报表预览
报表预览需要在SpringBoot中引入Java Web环境,并拷贝一些资源文件和模板,配置相应的属性才能实现在web页面上浏览报表,详细可以参考文章
https://www.ibm.com/developerworks/library/ba-birt-viewer-java-webapps/index.html#list3
常见问题
在开发过程中,在eclipse中经常修改后不能生效,应该是eclipse的问题,在eclipse中clean也不能生效,解决办法是在命令行中运行mvn clean package来生成包。
要理解脚本的执行顺序,为了提高效率,可以使用变量来缓存一些数据,比如在报表的脚本中需要调用后端的服务来获取数据,最好的办法是获取一次并存到global variable中,后续的脚本可以直接从reportContext中取获取这个全局变量来提高报表的渲染效率。使用方式如下:
reportContext.setPersistentGlobalVariable("变量名", 变量值);
reportContext.getPersistentGlobalVariable("变量名");
如果需要调试报表脚本,可以引入日志功能,简单输出日志的方法如下:
importPackage(Packages.java.lang);
System.out.println("你的日志信息");
网友评论