项目中需要做一个统计报表功能,实现各种Excel报表数据导出。要求表头能够动态配置,表数据通过存储过程实现,也要求能够动态配置。
技术选型:
由于之前在项目中使用过easypoi,相对于原生apache poi,能够用很少的代码写出Excel导入、导出功能,且API清晰好理解。因此优先选择了使用easypoi,验证功能需求能否实现。easypoi是基于apache poi开发,在此基础上进行了封装和扩展,特别复杂的功能就需要使用基础poi来开发了。
开发指南:https://opensource.afterturn.cn/doc/easypoi.html
实现思路:
由于配置的报表多是复杂多级表头,而easypoi对于动态表头生成只支持两级,简单来说就是表头最多两行,所以这种方式就只能放弃。改选用配置动态模板的方式,先做好模板,然后配置到数据表里。
实现步骤:
Maven pom中引入jar包
<dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-base</artifactId><version>3.0.1</version>
</dependency>
<dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-web</artifactId><version>3.0.1</version>
</dependency>
<dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-annotation</artifactId><version>3.0.1</version>
</dependency>
ReportController类: 如下代码仅显示主要步骤:
@RequestMapping("/exportExcel.html")@ResponseBodypublic void exportExcel(HttpServletResponse response, HttpSession session) {// 获取报表配置 ReportResultVo主要存储了 标题行数、模板路径位置、导出文件名称等ReportResultVo config = reportService.getReportConfig(id);TemplateExportParams params = new TemplateExportParams();// 标题开始行params.setHeadingStartRow(0);// 标题行数params.setHeadingRows(config.getHeadRowNum());// 设置sheetName,若不设置该参数,则使用得原本得sheet名称params.setSheetName("数据统计");// 获取报表内容 // 因为表数据是根据存储过程来实现的,不同的报表有不同的配置,// 所以使用Map<String,Object>格式来接收List<Map<String, Object>> reportBodyList = reportService.getReportBodyData(...);Map<String, Object> data = new HashMap<String, Object>();data.put("list", reportBodyList);// 获取模板文件路径// 这里有个很坑的地方,就是easypoi的API只能接收文件路径,无法读取文件流String filePath = 服务器上的某个路径或者项目中的某个路径// 设置模板路径params.setTemplateUrl(filePath);// 获取workbookWorkbook workbook = ExcelExportUtil.exportExcel(params, data);// exportFileName代表导出的文件名称ReportUtils.export(response, workbook, exportFileName);
ReportUtils类:
// Excel 导出 通过浏览器下载的形式public static void export(HttpServletResponse response, Workbook workbook, String fileName) throws IOException {response.setHeader("Content-Disposition","attachment;filename=" + new String(fileName.getBytes("UTF-8"), "iso8859-1"));response.setContentType("application/vnd.ms-excel;charset=UTF-8");response.setHeader("Pragma", "no-cache");response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expires", 0);BufferedOutputStream bufferedOutPut = new BufferedOutputStream(response.getOutputStream());workbook.write(bufferedOutPut);bufferedOutPut.flush();bufferedOutPut.close();}
模板样式:
模板以{{$fe:list
开头,以}}
结尾,代表变遍历数据的意思,每个字段前面的t.
前缀是easypoi指定的默认值。
获取的报表内容字段名称要与模板里的字段一一对应
List<Map<String, Object>> reportBodyList = new ArrayList<>();Map<String,Object> values = new HashMap<String,Object>();values.put(c1,总计);values.put(c2,10);values.put(c3,5);values.put(c4,8);values.put(c5,5);values.put(c6,8);values.put(c7,6);values.put(c8,3);reportBodyList.add(values);
导出的Excel结果如下:
到目前为止,已经可以实现需求了,但是实现的不够好,尤其是上面提到的easypoi无法读取文件流,只能从本地路径上获取文件模板,极大的限制了程序的灵活性。而生产环境中的项目大多都会使用文件存储服务器,比如fastdfs,而不是把模板上传到web服务器上的某个路径下。
还有别的解决办法吗?实在无法实现需求的话就只能使用apache poi了,但是这种方式改动太大,虽然可以灵活定制excel样式,但是实现要复杂的多。思考良久后,决定使用临时文件的方式解决这个问题。
实现思路:
从fastdfs中获取文件流后,写到本地临时目录,然后让easypoi从本地临时目录里读取模板文件,最后再删除临时文件。
关键代码如下:
@RequestMapping("/exportExcel.html")@ResponseBodypublic void exportExcel(HttpServletResponse response, HttpSession session) {......try{// 从fastDfs上获取文件流 (fileStorage.readFile自己封装的API)InputStream inputStream = fileStorage.readFile(filepath); // 模板临时目录String rootPath = session.getServletContext().getRealPath(“template_temp/”);// 临时文件路径名String filePath = rootPath + "_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + filename;tempFile = new File(filePath);// 保存到临时文件ReportUtils.saveTempFile(inputStream, tempFile);// 设置模板路径params.setTemplateUrl(filePath);// 获取workbookWorkbook workbook = ExcelExportUtil.exportExcel(params, data);// exportFileName代表导出的文件名称ReportUtils.export(response, workbook, exportFileName);} catch (Exception e) {throw new GeneralException(ErrorCode.REPORT_EXPORT_EXCEPTION);} finally {// 删除临时文件if (tempFile.exists()) {tempFile.delete();}}}
ReportUtils类:
// 保存到临时目录
public static void saveTempFile(InputStream inputStream, File tempFile) throws IOException {if(!tempFile.getParentFile().exists()){ //如果文件的目录不存在tempFile.getParentFile().mkdirs(); //创建目录}OutputStream os = new FileOutputStream(tempFile);byte[] b = new byte[2048];int length;while ((length = inputStream.read(b)) > 0) {os.write(b, 0, length);}os.flush();os.close();inputStream.close();
}
至此,代码实现较好的满足了动态配置的需要,如果大家有更好的方法,欢迎提出!
------------本文结束感谢您的阅读------------