编辑
2024-07-02
学习记录
00
请注意,本文编写于 275 天前,最后修改于 125 天前,其中某些信息可能已经过时。

目录

前提
xdocreport 简介
定义demo文件
表格填充
图片填充
后端代码
模板位置
核心代码
压缩包输出
进度条

前提

工作中需要导出word文件,由于之前没弄过,网上查询半天,最后采用了xdocreport的依赖进行导出word文件,效果不错,就是网上的资料比较少,list嵌套输出都查不到,踩了很多的坑,先简单介绍一下xdocreport吧

xdocreport 简介

xdocreportGithub地址, XDocReport means XML Document reporting. It's Java API to merge XML document created with MS Office (docx) or OpenOffice (odt), LibreOffice (odt) with a Java model to generate report and convert it if you need to another format (PDF, XHTML...).

翻译: XDocReport是指XML文档报告。Java API将使用MS Office(docx)或OpenOffice(odt)、LibreOffice(odt)创建的XML文档与Java模型合并以生成报告,并在需要时将其转换为其他格式(PDF、XHTML…)。

简单说就是得先定义一个demo.word文件,在需要填充的地方采用一些freemacker语法进行编译,然后java读取demo文件动态的填充内容,再输出文件。

定义demo文件

如何定义就是在需要添加内容的地方按ctrl+f9,会出现一个花括号,在花括号上右键选择编辑域,类别选择邮件合并,下面的域名选择MergeFleId,填写域名(格式一般都是${这里填你需要的属性名}),我用的是office的word,细节可能和wps有点不一样,但是整体都是相同的步骤。

1719889953286.png

表格填充

简单表格大概是这样

序号检查项部件状态

序号内我们需要先填入域名@before-row[#list checkList as check],表示开启最外层循环, 再起一个域名填入check_index+1,表示序号填充,最后起一个域名填入@after-row[/#list]表示关闭循环, 结果如下:

序号检查项部件状态
«@before-row[#list checkList as check]»«${check_index+1}»«@after-row[/#list]»«${check.partName}»«${check.faultTypeText}»

有时候如果后端数据的某个值为空会出现导出word失败,我们得为可能出现空值的字段进行验证

例如: «${check.faultTypeText!}» ,加上感叹号加上为空的话就不填充,当然还有其他if else的方法,为空时可以填充另外的内容。

图片填充

先在一个word中加入图片,然后选中图片,word上方选择插入-书签,定义这个图片的属性名,然后保存即可

简单操作之后我们就可以得到一个这样的word文件,这个word包含了简单填充,表格填充,复杂表格填充,图片填充,嵌套循环

后端代码

先引入依赖

xml
<dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency> <!-- <dependency>--> <!-- <groupId>org.apache.velocity</groupId>--> <!-- <artifactId>velocity</artifactId>--> <!-- <version>1.7</version>--> <!-- </dependency>--> <dependency> <groupId>fr.opensagres.xdocreport</groupId> <artifactId>xdocreport</artifactId> <version>2.0.2</version> </dependency>

提示

xdocreport的版本要非常注意,之前使用2.1.0版本,启动报错,提示找不到某个核心文件,点击文件一看最低java17,所以我改用了2.0.2

模板位置

demo.docx存放在resources目录下面的word文件夹中,配置pom

xml
<build> <resources> <resource> <directory>src/main/resources</directory> <filtering>false</filtering> <includes> <include>word/*.docx</include> </includes> </resource> </resources> </build>

核心代码

java
public void exportTaskRec(HttpServletResponse res, TaskRecExportReq req) { List<Task> tasks = this.listByIds(req.getTaskIds()); if (CollUtil.isEmpty(tasks)) { throw new SystemException(500, "导出失败,查询任务为空"); } LambdaQueryWrapper<AbnormalV> abnQueryWrapper = new LambdaQueryWrapper<>(); abnQueryWrapper.in(AbnormalV::getTaskSn, req.getTaskIds()); List<AbnormalV> abnormalVs = iAbnormalVService.list(abnQueryWrapper); if (CollUtil.isEmpty(abnormalVs)) { throw new SystemException(500, "导出失败,查询异常信息为空"); } Task task = tasks.get(0); //获取模板 String resourcesPath = TaskServiceImpl.class.getClassLoader().getResource("").getPath(); // 拼接得到word文件夹和demo.docx文件的路径 String filePath = resourcesPath + "word/demo1.docx"; File inputPath = new File(filePath); try (FileInputStream inputStream = new FileInputStream(inputPath)) { IXDocReport report = XDocReportRegistry .getRegistry() .loadReport(inputStream, TemplateEngineKind.Freemarker); FieldsMetadata metadata = report.createFieldsMetadata(); IContext context = report.createContext(); context.put("startTime", DateUtil.format(task.getStartTime(), "yyyy年mm月dd日 hh时MM分ss秒")); context.put("endTime", DateUtil.format(task.getEndTime(), "yyyy年mm月dd日 hh时MM分ss秒")); context.put("trainNo", task.getTrainNo()); context.put("taskId", task.getId()); List<Map<String, String>> checkList = new ArrayList<>(); for (AbnormalV abn : abnormalVs) { Map<String, String> map = new HashMap<>(); map.put("partName", abn.getPartName()); map.put("faultTypeText", abn.getFaultTypeText()); checkList.add(map); } context.put("checkList", checkList); //获取故障信息 List<Map<String, Object>> faultList = new ArrayList<>(); Map<String, List<AbnormalV>> groupMap = abnormalVs.stream().collect(Collectors.groupingBy(AbnormalV::getFaultPosName)); groupMap.forEach((k, v) -> { Map<String, Object> faultMap = new HashMap<>(); ByteArrayImageProvider baip = this.getImageProvider(v.get(0).getFaultPosImg()); faultMap.put("bigImg", baip); List<Map<String, Object>> errList = new ArrayList<>(); for (AbnormalV abn : v) { Map<String, Object> err = new HashMap<>(); err.put("id", abn.getFaultSn()); err.put("checkTime", DateUtil.format(abn.getCheckTime(), "yyyy年mm月dd日 hh时MM分ss秒")); err.put("vehicleNo", abn.getVehicleNo()); err.put("trainNo", abn.getTrainNo()); err.put("partName", abn.getPartName()); err.put("pointName", abn.getPointName()); err.put("faultTypeText", abn.getFaultTypeText()); err.put("refImg", this.getImageProvider(abn.getFaultRelImg())); err.put("smallImg", this.getImageProvider(abn.getFaultImg())); errList.add(err); } faultMap.put("errList", errList); faultList.add(faultMap); }); context.put("faultList", faultList); // 生成文件 // 设置响应类型为Word文档 metadata.addFieldAsImage("bigImg", "faultList[0].bigImg", NullImageBehaviour.RemoveImageTemplate); metadata.addFieldAsImage("refImg", "faultList[0].errList[0].refImg", NullImageBehaviour.RemoveImageTemplate); metadata.addFieldAsImage("smallImg", "faultList[0].errList[0].smallImg", NullImageBehaviour.RemoveImageTemplate); report.setFieldsMetadata(metadata); res.setHeader("Content-Disposition", "attachment; filename=机器人作业记录表.docx"); res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); // 处理并输出到响应流 report.process(context, res.getOutputStream()); } catch (Exception e) { e.printStackTrace(); throw new SystemException(500, e.getMessage()); } } public ByteArrayImageProvider getImageProvider(String url) { ByteArrayImageProvider bip=null; // 打开URL连接 try (InputStream in = new URL(url).openStream()) { // 将输入流转换为字节数组 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int read; while ((read = in.read(buffer)) != -1) { outputStream.write(buffer, 0, read); } byte[] imageBytes = outputStream.toByteArray(); bip=new ByteArrayImageProvider(imageBytes); } catch (Exception e) { e.printStackTrace(); log.error("获取图片失败:"+e.getMessage()); // 处理异常情况 } return bip; }

压缩包输出

支持多个word文件存入压缩包中,导出压缩包

java
public void exportTaskRec(HttpServletResponse res, TaskRecExportReq req) { List<Map<String,Object>> fileList=this.generateFileBytes(req.getTaskIds()); if (fileList.size()==1){ try (OutputStream outputStream = res.getOutputStream()) { res.setHeader("Content-Disposition", "attachment; filename="+URLEncoder.encode((String) fileList.get(0).get("fileName"),"utf-8")); res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"); // 将字节数组写入输出流 outputStream.write((byte[]) fileList.get(0).get("bytes")); // 刷新输出流,确保所有数据都被发送出去 outputStream.flush(); } catch (IOException e) { // 异常处理 throw new SystemException(500,"导出word文件失败!"); } }else { try (ServletOutputStream baos = res.getOutputStream(); ZipOutputStream zos = new ZipOutputStream(baos)) { res.setHeader("Content-Disposition", "attachment; filename="+URLEncoder.encode(DateUtil.format(new Date(),"yyyy_MM_dd_HH_mm_s压缩包")+".zip", "utf-8")); res.setHeader("Content-Type", "application/zip"); for (int i = 0; i < fileList.size(); i++) { byte[] wordBytes = (byte[]) fileList.get(i).get("bytes"); ZipEntry entry = new ZipEntry((String) fileList.get(i).get("fileName")); zos.putNextEntry(entry); zos.write(wordBytes); zos.closeEntry(); } zos.flush(); zos.close(); baos.flush(); } catch (IOException e) { // 处理可能的异常 e.printStackTrace(); // 实际应用中应使用合适的日志记录方式 throw new SystemException(500,"导出压缩包文件失败!"); } } } public List<Map<String,Object>> generateFileBytes(List<String> taskIds){ List<Map<String,Object>> fileList = new ArrayList<>(); List<Task> tasks = this.listByIds(taskIds); if (CollUtil.isEmpty(tasks)) { throw new SystemException(500, "导出失败,查询任务为空"); } LambdaQueryWrapper<AbnormalV> abnQueryWrapper = new LambdaQueryWrapper<>(); abnQueryWrapper.in(AbnormalV::getTaskSn, taskIds); List<AbnormalV> abnormalVs = iAbnormalVService.list(abnQueryWrapper); if (CollUtil.isEmpty(abnormalVs)) { throw new SystemException(500, "导出失败,查询异常信息为空"); } for (Task task : tasks) { try (InputStream inputStream = TaskServiceImpl.class.getResourceAsStream("/word/demo1.docx")) { List<AbnormalV> abnormalVList=abnormalVs.stream().filter(i->i.getTaskSn().equals(task.getId())).collect(Collectors.toList()); if (CollUtil.isEmpty(abnormalVList)){ continue; } IXDocReport report = XDocReportRegistry .getRegistry() .loadReport(inputStream, TemplateEngineKind.Freemarker); FieldsMetadata metadata = report.createFieldsMetadata(); IContext context = report.createContext(); context.put("startTime", DateUtil.format(task.getStartTime(), "yyyy年mm月dd日 hh时MM分ss秒")); context.put("endTime", DateUtil.format(task.getEndTime(), "yyyy年mm月dd日 hh时MM分ss秒")); context.put("trainNo", task.getTrainNo()); context.put("taskId", task.getId()); List<Map<String, String>> checkList = new ArrayList<>(); for (AbnormalV abn : abnormalVList) { Map<String, String> map = new HashMap<>(); map.put("partName", abn.getPartName()); map.put("faultTypeText", abn.getFaultTypeText()); checkList.add(map); } context.put("checkList", checkList); //获取故障信息 List<Map<String, Object>> faultList = new ArrayList<>(); Map<String, List<AbnormalV>> groupMap = abnormalVList.stream().collect(Collectors.groupingBy(AbnormalV::getFaultPosName)); groupMap.forEach((k, v) -> { Map<String, Object> faultMap = new HashMap<>(); ByteArrayImageProvider baip = this.getImageProvider(v.get(0).getFaultPosImg()); faultMap.put("bigImg", baip); List<Map<String, Object>> errList = new ArrayList<>(); for (AbnormalV abn : v) { Map<String, Object> err = new HashMap<>(); err.put("id", abn.getFaultSn()); err.put("checkTime", DateUtil.format(abn.getCheckTime(), "yyyy年mm月dd日 hh时MM分ss秒")); err.put("vehicleNo", abn.getVehicleNo()); err.put("trainNo", abn.getTrainNo()); err.put("partName", abn.getPartName()); err.put("pointName", abn.getPointName()); err.put("faultTypeText", abn.getFaultTypeText()); err.put("refImg", this.getImageProvider(abn.getFaultRelImg())); err.put("smallImg", this.getImageProvider(abn.getFaultImg())); errList.add(err); } faultMap.put("errList", errList); faultList.add(faultMap); }); context.put("faultList", faultList); // 生成文件 // 设置响应类型为Word文档 metadata.addFieldAsImage("bigImg", "faultList[0].bigImg", NullImageBehaviour.RemoveImageTemplate); metadata.addFieldAsImage("refImg", "faultList[0].errList[0].refImg", NullImageBehaviour.RemoveImageTemplate); metadata.addFieldAsImage("smallImg", "faultList[0].errList[0].smallImg", NullImageBehaviour.RemoveImageTemplate); report.setFieldsMetadata(metadata); // 处理并输出到响应流 ByteArrayOutputStream baos = new ByteArrayOutputStream(); report.process(context, baos); Map<String, Object> data = new HashMap<>(); data.put("bytes", baos.toByteArray()); data.put("fileName", task.getId()+".docx"); fileList.add(data); }catch (Exception e){ e.printStackTrace(); throw new SystemException(500,e.getMessage()); } } return fileList; } public ByteArrayImageProvider getImageProvider(String url) { ByteArrayImageProvider bip = null; // 打开URL连接 try (InputStream in = new URL(url).openStream()) { // 从输入流中读取原始图片 BufferedImage originalImage = ImageIO.read(in); // 创建一个新的缩放后的图片 BufferedImage resizedImage = new BufferedImage(378, 378, originalImage.getType()); Graphics2D graphics2D = resizedImage.createGraphics(); graphics2D.drawImage(originalImage, 0, 0, 378, 378, null); graphics2D.dispose(); // 将缩放后的图片转换为字节数组 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(resizedImage, "jpg", outputStream); byte[] imageBytes = outputStream.toByteArray(); bip = new ByteArrayImageProvider(imageBytes); } catch (Exception e) { e.printStackTrace(); log.error("获取图片失败:" + e.getMessage()); // 处理异常情况 } return bip; }

进度条

发起导出请求,返回一个唯一的id

public Map<String, Object> exportBatchRec(TaskQueryReq req) { List<Task> tasks = iTaskVService.getTaskList(req); if (CollUtil.isEmpty(tasks)){ throw new SystemException(500,"没有需要导出的数据"); } String exportId= IdUtil.simpleUUID(); redisUtil.set(RedisConst.exportNum+exportId,0,6,TimeUnit.HOURS); Map<String, Object> map = new HashMap<>(); map.put("total",tasks.size()); map.put("exportId", exportId); new Thread(() -> generateFileBytes(tasks, exportId)).start(); return map; }

处理任务

public List<FileData> generateFileBytes(List<Task> tasks, String exportId) { List<FileData> fileList = new ArrayList<>(); boolean useRedis = StrUtil.isNotBlank(exportId); // 创建线程池 ExecutorService executor = Executors.newFixedThreadPool(30); CompletionService<Void> completionService = new ExecutorCompletionService<>(executor); List<String> taskIds = tasks.stream().map(Task::getId).collect(Collectors.toList()); int batchSize = 300; List<List<String>> batchedTaskIds = IntStream.range(0, (taskIds.size() + batchSize - 1) / batchSize) .mapToObj(i -> taskIds.subList(i * batchSize, Math.min(i * batchSize + batchSize, taskIds.size()))) .collect(Collectors.toList()); List<AlgoFault> allAlgoFaults = new ArrayList<>(); // 对每个子列表进行查询 for (List<String> batch : batchedTaskIds) { LambdaQueryWrapper<AlgoFault> queryFaultWrapper = new LambdaQueryWrapper<>(); queryFaultWrapper.in(AlgoFault::getTaskSn, batch) .in(AlgoFault::getFaultStatus, 1, 2, 4) .eq(AlgoFault::getCheckMode, 0); List<AlgoFault> algoFaults = iAlgoFaultService.list(queryFaultWrapper); allAlgoFaults.addAll(algoFaults); } Map<String, List<AlgoFault>> handlerMap = allAlgoFaults.stream().collect(Collectors.groupingBy(AlgoFault::getTaskSn)); Set<String> handlerIds = handlerMap.keySet(); for (Task task : tasks) { String docdemo; List<AlgoFault> handlerFaults = new ArrayList<>(); if (handlerIds.contains(task.getId())) { docdemo = "/word/demo1.docx"; handlerFaults = handlerMap.get(task.getId()); } else { docdemo = "/word/demo2.docx"; } try (InputStream inputStream = TaskServiceImpl.class.getResourceAsStream(docdemo)) { IXDocReport report = XDocReportRegistry .getRegistry() .loadReport(inputStream, TemplateEngineKind.Freemarker); FieldsMetadata metadata = report.createFieldsMetadata(); IContext context = report.createContext(); context.put("startTime", DateUtil.format(task.getStartTime(), "yyyy年MM月dd日 hh时mm分ss秒")); context.put("endTime", DateUtil.format(task.getEndTime(), "yyyy年MM月dd日 hh时mm分ss秒")); context.put("trainNo", task.getTrainNo()); context.put("taskId", task.getId()); List<Map<String, Object>> checkList = new ArrayList<>(); List<Fault> faultList = new CopyOnWriteArrayList<>(); if (CollUtil.isNotEmpty(handlerFaults)) { Map<String, List<AlgoFault>> partGroupMap = handlerFaults.stream().collect(Collectors.groupingBy(AlgoFault::getPartName)); partGroupMap.forEach((k, v) -> { Map<String, Object> map = new HashMap<>(); map.put("partName", k); map.put("faultNum", v.size()); checkList.add(map); }); // 获取故障信息 Map<String, List<AlgoFault>> groupMap = handlerFaults.stream().collect(Collectors.groupingBy(AlgoFault::getFaultPosName)); groupMap.forEach((k, v) -> { Callable<Void> callTask = () -> { Fault fault = new Fault(); ByteArrayImageProvider bigImgProvider = getImageProvider(v.get(0).getFaultPosImg()); fault.setBigImg(bigImgProvider); List<Err> errList = new ArrayList<>(); for (AlgoFault abn : v) { Err err = new Err(); err.setId(abn.getFaultSn()); err.setCheckTime(DateUtil.format(abn.getCheckTime(), "yyyy年MM月dd日 hh时mm分ss秒")); err.setVehicleNo(abn.getVehicleNo()); err.setTrainNo(abn.getTrainNo()); err.setPartName(abn.getPartName()); err.setPointName(abn.getPointName()); err.setFaultTypeText(abn.getFaultTypeText()); err.setRefImg(getImageProvider(abn.getFaultRelImg())); err.setSmallImg(getImageProvider(abn.getFaultImg())); errList.add(err); } fault.setErrList(errList); faultList.add(fault); return null; }; completionService.submit(callTask); }); // 等待所有任务完成 for (int i = 0; i < groupMap.size(); i++) { try { completionService.take().get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } // 生成文件 metadata.addFieldAsImage("bigImg", "fault.bigImg", NullImageBehaviour.RemoveImageTemplate); metadata.addFieldAsImage("refImg", "err.refImg", NullImageBehaviour.RemoveImageTemplate); metadata.addFieldAsImage("smallImg", "err.smallImg", NullImageBehaviour.RemoveImageTemplate); report.setFieldsMetadata(metadata); } context.put("checkList", checkList); context.put("faultList", faultList); ByteArrayOutputStream baos = new ByteArrayOutputStream(); report.process(context, baos); FileData data = new FileData(); data.setBytes(baos.toByteArray()); data.setFileName(task.getId() + ".docx"); fileList.add(data); } catch (Exception e) { e.printStackTrace(); throw new SystemException(500, e.getMessage()); } if (useRedis) { setExportNum(exportId); } } executor.shutdown(); if (exportId!=null){ ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ZipOutputStream zos = new ZipOutputStream(baos)) { for (int i = 0; i < fileList.size(); i++) { byte[] wordBytes = fileList.get(i).getBytes(); ZipEntry entry = new ZipEntry(fileList.get(i).getFileName()); zos.putNextEntry(entry); zos.write(wordBytes); zos.closeEntry(); } zos.flush(); zos.close(); baos.flush(); } catch (IOException e) { // 处理可能的异常 e.printStackTrace(); // 实际应用中应使用合适的日志记录方式 throw new SystemException(500, "导出压缩包文件失败!"+e.getMessage()); } redisUtil.set(RedisConst.exportNum + exportId + ":data", baos.toByteArray(), 10, TimeUnit.MINUTES); } return fileList; }

轮询id获取进度

public int checkExportProgress(String exportId) { String key=RedisConst.exportNum+exportId; if (redisUtil.hasKey(key)){ return (int) redisUtil.get(key); } return -1; }

下载文件

public void downTaskRec(String exportId, HttpServletResponse res) { String key=RedisConst.exportNum+exportId; if (!redisUtil.hasKey(key)){ throw new SystemException(500, "导出压缩包文件失败!未找到导出内容"); } String datakey=key+":data"; byte[] fileArray =redisUtil.get(datakey,byte[].class); try (ServletOutputStream baos = res.getOutputStream();){ res.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(DateUtil.format(new Date(), "yyyy_MM_dd_HH_mm_s压缩包") + ".zip", "utf-8")); res.setHeader("Content-Type", "application/zip"); baos.write(fileArray); } catch (IOException e) { // 处理可能的异常 e.printStackTrace(); // 实际应用中应使用合适的日志记录方式 throw new SystemException(500, "导出压缩包文件失败!"+e.getMessage()); } redisUtil.delKey(datakey); redisUtil.delKey(key); }

本文作者:Weee

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!