SpringBoot整合POI-TL动态生成Word文档
1、背景
最近在项目开发过程中,遇到需要动态生成word文档的需求,特意研究了下,最后选择了比较好实现的POI-TL动态生成word文档,POI-TL使用Word模板来进行填充。poi-tl官网。
2、word模板文件
3、项目中pom.xml需要用到的依赖
<!-- poi-tl是基于Apache POI的Word模板引擎 --><dependency><groupId>com.deepoove</groupId><artifactId>poi-tl</artifactId><version>1.10.0</version></dependency><!-- 上面需要的依赖--><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml-schemas</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-scratchpad</artifactId><version>4.1.2</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.7</version></dependency>
4、生成文件需要用到的对象
4.1 授信单信息
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;@Schema(description = "管理后台 - 授信单打印 Response VO")
@Data
public class AmountCreditApplyPrintRespVO {@Schema(description = "申请单位名称")private String applyUnitName;@Schema(description = "申请日期")private String applyDateStr;@Schema(description = "额度类型(字典编码:amount_credit_apply_type):INVESTMENT:投资类;MINERAL_CATEGORY:矿产类;FINANCIAL_CATEGORY:财务类;HUMAN_RESOURCES_CATEGORY:人力资源类;SAFETY_AND_ENVIRONMENTAL_PROTECTION_CATEGORY:安全环保类;TECHNOLOGY:科技类;INFORMATIONIZATION_CATEGORY:信息化类;MARKETING:营销类;PRODUCTION_CATEGORY:生产类;OTHERS_TYPE:其他类;")private String typeName;@Schema(description = "企业性质(字典编码:amount_credit_apply_company_type)(STATE_OWNED_ENTERPRISE:国企;PRIVATE_ENTERPRISE:私企;GOVERNMENT_AGENCY:政府机构;OTHERS_TYPE:其他类;)")private String companyTypeName;@Schema(description = "申请事由")private String applyCause;@Schema(description = "额度使用情况及财务账务处理计划")private String plan;@Schema(description = "不纳入授信管理依据")private String excludingCreditDescription;@Schema(description = "附件路径")private String attachmentPath;@Schema(description = "授信申请明细信息")List<AmountCreditApplyDetailPrintRespVO> acadetailList;@Schema(description = "经办人")private String hdlr;@Schema(description = "手机号码")private String hdlrPhn;@Schema(description = "承办单位部门负责人意见")private String organizerDepartOpinion;@Schema(description = "承办单位信用管理部门意见")private String organizerCdOpinion;@Schema(description = "承办单位分管领导意见")private String organizerDeputyOpinion;@Schema(description = "承办单位总经理意见")private String organizerDirectorOpinion;@Schema(description = "逐级单位业务部门意见")private String stepUnitBusinessOpinion;@Schema(description = "逐级单位信用管理部门意见")private String stepUnitCreditOpinion;@Schema(description = "逐级单位分管领导意见")private String stepUnitDeputyChargeOpinion;@Schema(description = "逐级单位总经理意见")private String stepUnitDirectorOpinion;@Schema(description = "云钢股份业务归口管理部门承办意见")private String stepUnitCentralizedCbOpinion;@Schema(description = "云钢股份业务归口管理部门逐级意见")private String stepUnitCentralizedZjOpinion;
}
4.2 授信单详细信息
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;@Schema(description = "管理后台 - 授信单明细信息打印 Response VO")
@Data
public class AmountCreditApplyDetailPrintRespVO {@Schema(description = "序号")private String orderNum;@Schema(description = "客户/供应商类别(字典编码:sply_amt_crdt_appl_dtl_ptnrTp)(CUSTOMER:客户;SUPPLIER:供应商)")private String ptnrTpName;@Schema(description = "客商编码")private String partnerNumber;@Schema(description = "客商名称")private String partnerName;@Schema(description = "实际占用额(万元)")private BigDecimal actualAmount;@Schema(description = "原额度(万元)")private BigDecimal originalAmount;@Schema(description = "申请额度(万元)")private BigDecimal applyAmount;@Schema(description = "使用期限(开始和结束)")private String useDeadline;}
5、Controller层
@GetMapping("/printAmountCreditApply")
@Operation(summary = "打印授信单")
@PreAuthorize("@ss.hasPermission('sply:amount-credit-apply:export')")
@ApiAccessLog(operateType = EXPORT)
public void printAmountCreditApply(@RequestParam("id") Long id,HttpServletResponse response) throws Exception {amountCreditApplyService.printAmountCreditApply(id,response);
}
6、Service层逻辑
@Overridepublic void printAmountCreditApply(Long id, HttpServletResponse response) throws Exception {if (ObjectUtils.isEmpty(id)) {throw exception(AMOUNT_CREDIT_APPLY_ID_NOT_EXISTS);}AmountCreditApplyDO amountCreditApplyDO = amountCreditApplyMapper.selectById(id);List<AmountCreditApplyDetailDO> acadetialList = amountCreditApplyDetailMapper.getAmountCreditApplyDetailListByApplyId(id);//获得指定流程实例的任务列表List<BpmTaskRespDTO> btdtoList = bpmTaskApi.getTaskListByProcessInstanceId(amountCreditApplyDO.getProcessInstanceId()).getData();if (ObjectUtils.isEmpty(amountCreditApplyDO)) {throw exception(AMOUNT_CREDIT_APPLY_NOT_EXISTS);} else if (CollectionUtils.isEmpty(acadetialList)) {throw exception(AMOUNT_CREDIT_APPLY_DETAIL_NOT_EXISTS);}// 先封装填充word的数据AmountCreditApplyPrintRespVO acaprvo = BeanUtils.toBean(amountCreditApplyDO,AmountCreditApplyPrintRespVO.class);acaprvo.setApplyDateStr(DateUtils.localDateTimeToStr(amountCreditApplyDO.getApplyDate(),DateUtils.YYYY_MM_DD));acaprvo.setTypeName(AmountCreditApplyTypeEnum.getEnumByCode(amountCreditApplyDO.getType()).getName());acaprvo.setCompanyTypeName(AmountCreditApplyCompanyTypeEnum.getEnumByCode(amountCreditApplyDO.getCompanyType()).getName());checkAndInitAmountCreditApplyPrintRespVO(acaprvo); // 检查属性是否为nullif (CollectionUtils.isNotEmpty(btdtoList)) {for (BpmTaskRespDTO item : btdtoList) {//UserSimpleBaseDTO usbdto = item.getAssigneeUser();if (item.getName().contains("承办单位部门负责人审批") && StringUtils.isBlank(acaprvo.getOrganizerDepartOpinion())) {acaprvo.setOrganizerDepartOpinion(initApprovalOpinion(item.getReason(),"测试","测试",item.getEndTime())); // 承办单位部门负责人意见} else if (item.getName().contains("承办单位信用管理部门审批") && StringUtils.isBlank(acaprvo.getOrganizerCdOpinion())) {acaprvo.setOrganizerCdOpinion(initApprovalOpinion(item.getReason(),"测试1","测试1",item.getEndTime())); // 承办单位信用管理部门意见} else if (item.getName().contains("承办单位分管领导审批") && StringUtils.isBlank(acaprvo.getOrganizerDeputyOpinion())) {acaprvo.setOrganizerDeputyOpinion(initApprovalOpinion(item.getReason(),"测试2","测试2",item.getEndTime())); // 承办单位分管领导意见} else if (item.getName().contains("承办单位总经理审批") && StringUtils.isBlank(acaprvo.getOrganizerDirectorOpinion())) {acaprvo.setOrganizerDirectorOpinion(initApprovalOpinion(item.getReason(),"测试3","测试3",item.getEndTime())); // 承办单位总经理意见}else if (item.getName().contains("云钢股份业务归口管理部门承办审批") && StringUtils.isBlank(acaprvo.getStepUnitCentralizedCbOpinion())) {acaprvo.setStepUnitCentralizedCbOpinion(initApprovalOpinion(item.getReason(),"测试4","测试4",item.getEndTime())); // 云钢股份业务归口管理部门承办意见}else if (item.getName().contains("逐级单位业务部门审批") && StringUtils.isBlank(acaprvo.getStepUnitBusinessOpinion())) {acaprvo.setStepUnitBusinessOpinion(initApprovalOpinion(item.getReason(),"测试5","测试5",item.getEndTime())); // 逐级单位业务部门意见} else if (item.getName().contains("逐级单位信用管理部门审批") && StringUtils.isBlank(acaprvo.getStepUnitCreditOpinion())) {acaprvo.setStepUnitCreditOpinion(initApprovalOpinion(item.getReason(),"测试6","测试6",item.getEndTime())); // 逐级单位信用管理部门意见} else if (item.getName().contains("逐级单位分管领导审批") && StringUtils.isBlank(acaprvo.getStepUnitDeputyChargeOpinion())) {acaprvo.setStepUnitDeputyChargeOpinion(initApprovalOpinion(item.getReason(),"测试7","测试7",item.getEndTime())); // 逐级单位分管领导意见}else if (item.getName().contains("逐级单位总经理审批") && StringUtils.isBlank(acaprvo.getStepUnitDirectorOpinion())) {acaprvo.setStepUnitDirectorOpinion(initApprovalOpinion(item.getReason(),"测试8","测试8",item.getEndTime())); // 逐级单位总经理意见}else if (item.getName().contains("云钢股份业务归口管理部门逐级审批") && StringUtils.isBlank(acaprvo.getStepUnitCentralizedZjOpinion())) {acaprvo.setStepUnitCentralizedZjOpinion(initApprovalOpinion(item.getReason(),"测试9","测试9",item.getEndTime())); // 云钢股份业务归口管理部门逐级意见}}}// 设置授信单详情List<AmountCreditApplyDetailPrintRespVO> acadprBeans = new ArrayList<>();for (int i = 0; i < acadetialList.size();i++) {AmountCreditApplyDetailDO item = acadetialList.get(i);AmountCreditApplyDetailPrintRespVO bean = BeanUtils.toBean(item, AmountCreditApplyDetailPrintRespVO.class);bean.setOrderNum(String.valueOf(i+1));bean.setPtnrTpName(AmountCreditApplyDetailPtnrTpEnum.getEnumByCode(item.getPtnrTp()).getName());bean.setUseDeadline(initUseDeadline(item.getStartDate(),item.getEndDate(),"至")); // 使用期限(开始和结束)checkAndInitAmountCreditApplyDetailPrintRespVO(bean); // 检查授信单详情对象属性是否为nullacadprBeans.add(bean);}acaprvo.setAcadetailList(acadprBeans);Map<String,Object> dataMap = new ConcurrentHashMap<>();List<Map<String,Object>> detailMap = new ArrayList<>();dataMap = JsonUtils.parseObject(JSON.toJSONString(acaprvo, SerializerFeature.DisableCircularReferenceDetect),Map.class);detailMap = (List<Map<String, Object>>) JSON.parse(JSON.toJSONString(acadprBeans));dataMap.put("acadetailList",detailMap);/*** 4. Configure类是该库中的一个配置类,其作用是提供了一些全局的配置选项* (1) useSpringEL() 开启El表达式{{ }} word模板中的数据就以这个表达式传递数据 例如:{{companyName}};也可以调用buildGramer("${", "}") 可以修改模板为${}* (2) bind() 绑定标记需要循环的数据* (3) 实现表格行循环的策略 HackLoopTableRenderPolicy 不同poi版本实现行循环的策略不一样;当前案例是poi-tl 1.9.1版本* 1.9.x版本:HackLoopTableRenderPolicy 1.10.x以后的版本:LoopRowTableRenderPolicy*/ConfigureBuilder configureBuilder = Configure.builder().useSpringEL().bind("acadetailList", new LoopRowTableRenderPolicy());Configure config = configureBuilder.build();String fileName = "云南铜业股份有限公司调整额度申请表" + ".docx";ExportWordUtil.exportWordDocx(response, fileName, "printAmountCreditApplyTemplate.docx","static"+ File.separator +"templates", dataMap, config,"UTF-8");}/*** 初始化审批意见内容* @param reason 审批意见* @param nickname 处理人* @param deptName 部门名称* @param endTime 处理时间* @return*/private String initApprovalOpinion(String reason, String nickname, String deptName,LocalDateTime endTime) {String res = "";if (StringUtils.isNotBlank(reason) && StringUtils.isNotBlank(nickname) && StringUtils.isNotBlank(deptName) && ObjectUtils.isNotEmpty(endTime)) {res = reason + System.lineSeparator() + deptName + " " + nickname + " " + DateUtils.localDateTimeToStr(endTime,DateUtils.YYYY_MM_DD_HH_MM_SS);}return res;}/*** 对使用期限进行格式处理* @param startDate 使用期限开始时间* @param endDate 使用期限结束时间* @param splice 中间的拼接字符* @return*/private String initUseDeadline(LocalDateTime startDate,LocalDateTime endDate,String splice) {String res = "";if (StringUtils.isBlank(splice)) {splice = "至";}if (ObjectUtils.isNotEmpty(startDate) && ObjectUtils.isNotEmpty(endDate)) {res = DateUtils.localDateTimeToStr(startDate,DateUtils.YYYY_MM_DD) + " " + splice + " " + DateUtils.localDateTimeToStr(endDate,DateUtils.YYYY_MM_DD);}return res;}/*** 检查授信单属性是否有值,没有值赋值为空字符,word表达式中如果属性没有值会报错* @param acaprvo 打印的word属性对象*/private void checkAndInitAmountCreditApplyPrintRespVO(AmountCreditApplyPrintRespVO acaprvo) {if (StringUtils.isBlank(acaprvo.getApplyUnitName())) {acaprvo.setApplyUnitName("");}if (StringUtils.isBlank(acaprvo.getApplyDateStr())) {acaprvo.setApplyDateStr("");}if (StringUtils.isBlank(acaprvo.getTypeName())) {acaprvo.setTypeName("");}if (StringUtils.isBlank(acaprvo.getCompanyTypeName())) {acaprvo.setCompanyTypeName("");}if (StringUtils.isBlank(acaprvo.getApplyCause())) {acaprvo.setApplyCause("");}if (StringUtils.isBlank(acaprvo.getPlan())) {acaprvo.setPlan("");}if (StringUtils.isBlank(acaprvo.getExcludingCreditDescription())) {acaprvo.setExcludingCreditDescription("");}if (StringUtils.isBlank(acaprvo.getAttachmentPath())) {acaprvo.setAttachmentPath("");}if (StringUtils.isBlank(acaprvo.getHdlr())) {acaprvo.setHdlr("");}if (StringUtils.isBlank(acaprvo.getHdlrPhn())) {acaprvo.setHdlrPhn("");}if (StringUtils.isBlank(acaprvo.getOrganizerDepartOpinion())) {acaprvo.setOrganizerDepartOpinion("");}if (StringUtils.isBlank(acaprvo.getOrganizerCdOpinion())) {acaprvo.setOrganizerCdOpinion("");}if (StringUtils.isBlank(acaprvo.getOrganizerDeputyOpinion())) {acaprvo.setOrganizerDeputyOpinion("");}if (StringUtils.isBlank(acaprvo.getOrganizerDirectorOpinion())) {acaprvo.setOrganizerDirectorOpinion("");}if (StringUtils.isBlank(acaprvo.getStepUnitBusinessOpinion())) {acaprvo.setStepUnitBusinessOpinion("");}if (StringUtils.isBlank(acaprvo.getStepUnitCreditOpinion())) {acaprvo.setStepUnitCreditOpinion("");}if (StringUtils.isBlank(acaprvo.getStepUnitDeputyChargeOpinion())) {acaprvo.setStepUnitDeputyChargeOpinion("");}if (StringUtils.isBlank(acaprvo.getStepUnitDirectorOpinion())) {acaprvo.setStepUnitDirectorOpinion("");}if (StringUtils.isBlank(acaprvo.getStepUnitCentralizedCbOpinion())) {acaprvo.setStepUnitCentralizedCbOpinion("");}if (StringUtils.isBlank(acaprvo.getStepUnitCentralizedZjOpinion())) {acaprvo.setStepUnitCentralizedZjOpinion("");}}/*** 检查授信单详情对象属性是否有为null的,如果有则重新赋值为空字符* @param bean 授信单详情对象*/private void checkAndInitAmountCreditApplyDetailPrintRespVO(AmountCreditApplyDetailPrintRespVO bean) {if (StringUtils.isBlank(bean.getOrderNum())) {bean.setOrderNum("");}if (StringUtils.isBlank(bean.getPtnrTpName())) {bean.setPtnrTpName("");}if (StringUtils.isBlank(bean.getPartnerNumber())) {bean.setPartnerNumber("");}if (StringUtils.isBlank(bean.getPartnerName())) {bean.setPartnerName("");}if (ObjectUtils.isEmpty(bean.getActualAmount())) {bean.setActualAmount(BigDecimal.ZERO);}if (ObjectUtils.isEmpty(bean.getOriginalAmount())) {bean.setOriginalAmount(BigDecimal.ZERO);}if (ObjectUtils.isEmpty(bean.getApplyAmount())) {bean.setApplyAmount(BigDecimal.ZERO);}if (StringUtils.isBlank(bean.getUseDeadline())) {bean.setUseDeadline("");}}
7、工具类方法
package com.zt.plat.module.capital.utils.wordUtil;import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.util.PoitlIOUtils;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Map;@Slf4j
public class ExportWordUtil {private String encoding = "UTF-8";private String exportPath = "D:\\data";/*** poi-tl导出word* @param response 响应设置* @param fileName 导出文件名称* @param tplName 模板名称* @param tplPath 模版路径* @param data 设置的数据* @param config 配置* @param encoding 编码方式* @throws Exception*/public static void exportWordDocx(HttpServletResponse response, String fileName, String tplName,String tplPath, Map<String, Object> data, Configure config, String encoding) throws Exception {response.reset();response.setHeader("Access-Control-Allow-Origin", "*");response.setHeader("Access-Control-Expose-Headers","content-disposition"); // 设置方便前端获取文件名称response.setCharacterEncoding("UTF-8");response.setContentType("application/octet-stream");response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));//获取文件的路径String path = tplPath + File.separator + tplName;String filePathD = URLDecoder.decode(path, "UTF-8");//如果路径中带有中文会被URLEncoder,因此这里需要解码InputStream inputStream = ExportWordUtil.class.getClassLoader().getResourceAsStream(filePathD);XWPFTemplate template = XWPFTemplate.compile(inputStream, config).render(data);OutputStream out = response.getOutputStream();BufferedOutputStream bos = new BufferedOutputStream(out);template.write(bos);bos.flush();out.flush();PoitlIOUtils.closeQuietlyMulti(template, bos, out);}}
总结:1、word模版一定、一定、一定要使用Microsoft Word来进行生成,不能使用WPS或其他工具生成。2、渲染模板绑定的对象,对象属性值不能为null,值为null渲染模板会失败。