前言:
今天姐妹们对“java导出为word怎么循环”可能比较关注,各位老铁们都需要知道一些“java导出为word怎么循环”的相关知识。那么小编同时在网络上收集了一些有关“java导出为word怎么循环””的相关内容,希望同学们能喜欢,朋友们快快来学习一下吧!1 模板填充介绍
aspose对表格进行模板填充 和xdocreport一样 使用word邮件合并的MergeField域插入变量 区别就是语法差异
但是语法是aspose简单 而且功能更强大更方便 而且通过xdocreport填充变量生成的复选框 单选框 只能在Microsoft Office套件的word中打开才正常显示 用WPS打开就看不到了 不能兼容 用aspose就可以兼容2种word工具 而且用WPS打开后 对复选框进行点击 可以切换选中状态
而且对于图片的填充 aspose也支持变量方式 而xdocreport仅支持用图片放在模板中 再用标签去设置变量名 很不方便 而且需要循环填充图片时 xdocreport就更复杂了 谁用谁哭泣 而aspose就方便多了 仅需要设置被替换的字符 然后克隆行 找到需要替换的字符的行列坐标就可以进行图片的插入了
还有一种对表格循环填充的方法 是进行表格行克隆 再对应的往单元格塞数据 适用于带图片的单元格行的循环填充
1.1 邮件合并的MergeField域 设置需要被替换的变量
可以在域这个选项上右键 添加到快捷访问工具栏 这样以后就可以直接点击左上角的快捷按钮进行操作了
而且创建了一个 其余可以复制后编辑 点击后将光标放在变量中间 然后右键 可以进入编辑域
1.2 需要处理循环行中 带有图片的单元格时 可以使用克隆行后进行变量坐标锁定后插入图片
直接在word中填写变量名称 实现循环导出时就找到需要克隆的行 进行克隆后 对于文字进行替换变量字符 对于图片进行变量删除后插入图片
1.3 嵌套表格
对于这种一行中的单元格 还嵌套了一个表格 且需要循环导出时 也可以实现填充 图片显示了填充的变量域的设置方法 代码实现方法详见下文
1.4 循环填充数据语法
在word填充中 通常在表格中含有子表 且子表的数据行是动态的 这时就需要用到循环的语法 来动态的扩充行
而且这个语法也适用于非表格的文字循环 也就是文字段落 需要循环填充的话 也可以使用 虽然语法的名字看起来像专门为表格设置的 但其实对于表格和文字段落的循环填充 都可以用
循环的语法 可以参见1.3中的嵌套表格 也是在MergeField的域中 设置变量 "TableStart:list" "TableEnd:list"
开始循环的变量 放在需要开始循环的位置前 以"TableStart"开头 中间加":" 后面跟上变量名
结束循环的变量 放在需要结束循环的位置后 以"TableEnd"开头 中间加":" 后面跟上变量名
用一对循环语法包裹需要循环的内容 即可以实现循环插入
2 填充的代码实现
对于各种类型的填充 我已经封装了方法 以便使用者仅需要关注填充的数据和模板设置即可 无需再手动处理各种类型的填充逻辑
需要根据各种类型进行详细研究的填充实现逻辑的 可以逐行拆解去理解下 还是有点复杂的
这里简单介绍下填充的入口方法fillTpl(LinkedHashMap<String, Object> dataMap, String resourceClassPath, Map<String,Integer>... tableIndexFieldMaps
其中
第1个参数dataMap 指的是需要填充的变量 以LinkedHashMap类型存入 为什么我用了该类型 因为对于需要图片表格循环时 是需要定位最后一行进行克隆后复制的 以便遍历该map时 取到对应的map值也是最后才取到 确保最后取到的一行是真确的那行
所有需要单个填充的变量 都存入该map 如果数据原本是对应 可以使用 BeanUtil.beanToMap(obj)方法进行转换后加入到dataMap所有需要循环填充的list集合 也存到该map 但是对于不同填充方式需要注意存入dataMap值的list类型--对于需要用占位符 也就是MergeField域来填充的 使用常规的ArrayList类型即可--对于需要用到字符替换 也就是需要循环填充带图片的表格行时 请使用LinkedList类型(这个类型的参数 仅限于最后一个表格行的循环填充 切记 具体原因上面已经讲到过了 后续不再赘述)
第2个参数resourceClassPath 也就是模板文件在resources目录中的位置 比如模板在此 那就传入"template/complexTabel.docx"
第3个参数tableIndexFieldMaps 表格索引位和传入dataMap的key的映射关系 可选参数 是专门用来指定dataMap中LinkedList类型的 因为克隆行时 需要指定表格的索引位 有时我们在填充模板时 表格可能是有上下2个或多个组成的 因此需要指定这个映射关系
所有需要填充照片的 变量类型都使用byte[]
fillTpl方法的返回类型为Pair key的值为word文件的字节数组 value的值为该word文件的页数
因为有时候会遇到更加复杂的需求 需要循环填充word 然后再将各个word合并成1个 还要在文档头部生成非标准的目录 这时就需要用到每个文档的页数 以便手动统计各目录对应的起始页码
对应单选 复选框的实现 是基于word中的字符来实现的 aspose填充时 需要设置字体和编码
此处如果 有需要其他字符的 可以自己在方法中扩充
下面是封装的工具类和方法的具体代码
package com.example.support.tool;import cn.hutool.core.bean.BeanUtil;import cn.hutool.core.collection.CollUtil;import cn.hutool.core.io.FileUtil;import cn.hutool.core.io.resource.ClassPathResource;import cn.hutool.core.io.resource.Resource;import cn.hutool.core.lang.Assert;import cn.hutool.core.lang.Pair;import cn.hutool.core.map.BiMap;import cn.hutool.core.util.ArrayUtil;import cn.hutool.core.util.ClassUtil;import cn.hutool.core.util.RandomUtil;import cn.hutool.core.util.StrUtil;import com.aspose.words.*;import com.aspose.words.net.System.Data.DataRelation;import com.aspose.words.net.System.Data.DataRow;import com.aspose.words.net.System.Data.DataSet;import com.aspose.words.net.System.Data.DataTable;import lombok.SneakyThrows;import java.io.ByteArrayOutputStream;import java.io.InputStream;import java.util.List;import java.util.*;/** * @author LWB * @Description aspose工具 */public class AsposeTool { /** 复选框 勾选 */ public static final String BOX_CHECKED = "BOX_CHECKED"; /** 复选框 未勾选 */ public static final String BOX_UNCHECKED = "BOX_UNCHECKED"; /** 单选框 勾选 */ public static final String RADIO_CHECKED = "RADIO_CHECKED"; /** 单选框 未勾选 */ public static final String RADIO_UNCHECKED = "RADIO_UNCHECKED"; /** 主表与子表的关联ID:嵌套表时需要 */ private static final String MAIN_SUB_TABLE_RELATION_KEY = "relationKey"; /** * 获取单元格的文本并移除"\f"分页符 * @param cell 单元格对象 * @return */ public static String getText(Cell cell){ String text = cell.getText(); return StrUtil.isBlank(text) ? "" : text.trim().replace("\f",""); } /** * 获取单元格种的图片 <br/> * 可能一个单元格 含有多个图片 * @param cell 单元格对象 * @return null-无图片 */ @SneakyThrows public static List<byte[]> getImg(Cell cell){ NodeCollection shapeNodes = cell.getChildNodes(NodeType.SHAPE, true); if(shapeNodes.getCount() < 1) return null; List<byte[]> list = new ArrayList<>(); for (Object childNode : shapeNodes) { Shape shape = (Shape)childNode; byte[] imageBytes = shape.getImageData().getImageBytes(); list.add(imageBytes); } return list; } /** * 创建页脚页码 * @param doc * @param withTotalPage 是否显示共 x 页 默认 false */ @SneakyThrows public static void addFooterPageNum(Document doc, boolean... withTotalPage){ boolean hasTotalPage = false; if(ArrayUtil.isNotEmpty(withTotalPage)) hasTotalPage = withTotalPage[0]; //创建页脚 页码 HeaderFooter footer = new HeaderFooter(doc, HeaderFooterType.FOOTER_PRIMARY); doc.getFirstSection().getHeadersFooters().add(footer); //页脚段落 Paragraph footerpara = new Paragraph(doc); footerpara.getParagraphFormat().setAlignment(ParagraphAlignment.CENTER); if(hasTotalPage){ Run footerparaRun = new Run(doc,"共 "); footerparaRun.getFont().setName("宋体"); footerparaRun.getFont().setSize(9.0);//小5号字体 footerpara.appendChild(footerparaRun); footerpara.appendField(FieldType.FIELD_NUM_PAGES,true);//总页码 footerparaRun = new Run(doc," 页,第 "); footerparaRun.getFont().setName("宋体"); footerparaRun.getFont().setSize(9.0);//小5号字体 footerpara.appendChild(footerparaRun); footerpara.appendField(FieldType.FIELD_PAGE,true);//当前页码 footerparaRun = new Run(doc," 页"); footerparaRun.getFont().setName("宋体"); footerparaRun.getFont().setSize(9.0);//小5号字体 footerpara.appendChild(footerparaRun); }else{ Run footerparaRun = new Run(doc,"第 "); footerparaRun.getFont().setName("宋体"); footerparaRun.getFont().setSize(9.0);//小5号字体 footerpara.appendChild(footerparaRun); footerpara.appendField(FieldType.FIELD_PAGE,true);//当前页码 footerparaRun = new Run(doc," 页"); footerparaRun.getFont().setName("宋体"); footerparaRun.getFont().setSize(9.0);//小5号字体 footerpara.appendChild(footerparaRun); } footer.appendChild(footerpara); } /** * 填充循环数据模板(替换字符法) <br/> * 仅支持表格 填充内容类型:文字和图片都支持 * @param list 数据集合 * @param doc aspose.doc对象 * @param tableIndex 表格的索引顺序 * @return word表格数据DataTable */ @SneakyThrows public static void fillListData(List list, Document doc,int tableIndex) { DocumentBuilder builder = new DocumentBuilder(doc); Table table = (Table)doc.getChild(NodeType.TABLE, tableIndex, true); FindReplaceOptions findReplaceOptions = new FindReplaceOptions(); findReplaceOptions.setFindWholeWordsOnly(true); RowCollection rows = table.getRows(); int startRowIndex = rows.getCount() - 1; //先复制行 for (Object obj : list) { Node deepClone = table.getLastRow().deepClone(true); table.getRows().add(deepClone); } table.getLastRow().remove();//将多余的1行删除 //再填充数据(更新行的获取) rows = table.getRows(); //将第1个数据行的被替换字段与行索引形成map映射 CellCollection cells = rows.get(rows.getCount()-1).getCells(); int columnCount = cells.getCount(); BiMap<String,Integer> fieldNameIndexMap = new BiMap<>(new HashMap<>()); for (int colIndex = 0; colIndex < columnCount; colIndex++) { String text = AsposeTool.getText(cells.get(colIndex)); if(StrUtil.isNotBlank(text)) text = text.trim(); fieldNameIndexMap.put(text,colIndex); } //表格的第一个数据行索引 = 标题行的数量 int curRowIndex = startRowIndex; for (int i = 0; i < list.size(); i++) { Range range = rows.get(curRowIndex).getRange(); Object obj = list.get(i); Map<String, Object> beanToMap = BeanUtil.beanToMap(obj); Set<String> keySet = beanToMap.keySet(); for (String key : keySet) { Object val = beanToMap.get(key); String valStr = ""; if(null != val && ClassUtil.isSimpleValueType(val.getClass())) valStr = val.toString(); //先将被替换字段用空字符代替,再插入图片 if(val instanceof byte[]){ range.replace(key,"",findReplaceOptions); builder.moveToCell(tableIndex,curRowIndex,fieldNameIndexMap.get(key),0); builder.insertImage((byte[])val); }else{ range.replace(key, valStr, findReplaceOptions); } } curRowIndex++; } } /** * 填充循环数据模板(占位符法) <br/> * 表格和段落都支持 填充内容仅支持文字 * @param list * @param tableName * @return */ public static DataTable fillListData(List list, String tableName) { //创建DataTable DataTable dataTable = new DataTable(tableName); //绑定字段 if(CollUtil.isNotEmpty(list)){ Object obj = list.get(0); Map<String, Object> beanToMap = BeanUtil.beanToMap(obj); Set<String> keySet = beanToMap.keySet(); for (String key : keySet) { dataTable.getColumns().add(key); } } //填充数据 for (Object obj : list){ //创建DataRow,封装该行数据 DataRow dataRow = dataTable.newRow(); Map<String, Object> beanToMap = BeanUtil.beanToMap(obj); Set<String> keySet = beanToMap.keySet(); for (String key : keySet) { Object value = beanToMap.get(key); dataRow.set(key,value); } dataTable.getRows().add(dataRow); } return dataTable; } /** * 填充嵌套循环模板(占位符法) <br/> * @param dataList * @param tableName * @return */ public static DataSet fillNestedListData(List dataList, String tableName) { DataSet dataSet = new DataSet(); //创建DataTable DataTable mainTable = new DataTable(tableName); dataSet.getTables().add(mainTable); Map<String,DataTable> subTableMap = new HashMap<>(); //绑定主表字段 if(CollUtil.isNotEmpty(dataList)){ //设置主表与子表的关联ID字段 mainTable.getColumns().add(MAIN_SUB_TABLE_RELATION_KEY); Object obj = dataList.get(0); Map<String, Object> beanToMap = BeanUtil.beanToMap(obj); Set<String> keySet = beanToMap.keySet(); for (String key : keySet) { Object value = beanToMap.get(key); if(value instanceof List){ //集合 创建子表 List subList = (List) value; //子表 DataTable subTable = new DataTable(key); dataSet.getTables().add(subTable); //设置主表与子表的关联ID字段 subTable.getColumns().add(MAIN_SUB_TABLE_RELATION_KEY); //绑定子表字段 if(CollUtil.isNotEmpty(subList)){ Object subObj = subList.get(0); Map<String, Object> subMap = BeanUtil.beanToMap(subObj); Set<String> subKeySet = subMap.keySet(); for (String subKey : subKeySet) { subTable.getColumns().add(subKey); } subTableMap.put(key,subTable); } }else{//非集合 直接添加字段 mainTable.getColumns().add(key); } } } //填充数据 int relationKey = 0; for (Object obj : dataList){ //创建DataRow,封装该行数据 DataRow dataRow = mainTable.newRow(); //给关联ID赋值 dataRow.set(MAIN_SUB_TABLE_RELATION_KEY,relationKey); Map<String, Object> beanToMap = BeanUtil.beanToMap(obj); Set<String> keySet = beanToMap.keySet(); for (String key : keySet) { Object value = beanToMap.get(key); if(value instanceof List){ List subList = (List) value; //填充数据 for (Object subObj : subList){ //创建DataRow,封装该行数据 DataTable subTable = subTableMap.get(key); DataRow subDataRow = subTable.newRow(); //给关联ID赋值 subDataRow.set(MAIN_SUB_TABLE_RELATION_KEY,relationKey); Map<String, Object> subMap = BeanUtil.beanToMap(subObj); Set<String> subKeySet = subMap.keySet(); for (String subKey : subKeySet) { Object subValue = subMap.get(subKey); subDataRow.set(subKey,subValue); } subTable.getRows().add(subDataRow); } }else{ dataRow.set(key,value); } } mainTable.getRows().add(dataRow); relationKey++; } //建立主表子表的关联 subTableMap.forEach((subTableName,subTable) -> dataSet.getRelations().add(new DataRelation(RandomUtil.randomString(8), mainTable.getColumns().get(MAIN_SUB_TABLE_RELATION_KEY), subTable.getColumns().get(MAIN_SUB_TABLE_RELATION_KEY)))); return dataSet; } /** * 填充模板(替换字符法) <br/> * 注意: <br/> * 需要程序逐行复制表格来生成数据的(替换字符方法),dataMap的value类型请使用LinkedList(仅支持所在表格的最后一行的情况) <br/> * 需要用占位符来循环生成表格数据的,dataMap的value类型请使用List * @param dataMap 填充的数据 * @param resourceClassPath 模板在resources中的地址 like "template/asposeTpl.docx" * @param tableIndexFieldMaps word中的表格 需要复制行来完成表格填充时 即用到LinkedList dataMap数据的字段key 和表格的索引位置 map * @return */ @SneakyThrows public static Pair<byte[],Integer> fillTpl(LinkedHashMap<String, Object> dataMap, String resourceClassPath, Map<String,Integer>... tableIndexFieldMaps){ Map<String,Integer> tableIndexFieldMap = new HashMap<>(); if(ArrayUtil.isNotEmpty(tableIndexFieldMaps)) tableIndexFieldMap = Arrays.stream(tableIndexFieldMaps).findFirst().get(); //获取填充模板 Resource resource = new ClassPathResource(resourceClassPath); try( InputStream tplIns = resource.getStream(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); ){ Document doc = new Document(tplIns); DocumentBuilder builder = new DocumentBuilder(doc); String[] keyArr = dataMap.keySet().stream().toArray(String[]::new); Map<String, Object> textMap = new HashMap(); for (String key : keyArr) { Object value = dataMap.getOrDefault(key, null); if(value instanceof LinkedList){//LinkedList 复制行生成表格数据专用类型 int tableIndex = tableIndexFieldMap.getOrDefault(key, -1); Assert.isTrue(tableIndex > -1,"请在tableIndexFieldMap中传入对应表格数据字段{}在word种的表格索引顺序",key); AsposeTool.fillListData((List) value,doc,tableIndex); }else if(value instanceof List){//使用DataSet来动态生成循环数 可嵌套循环数据 DataSet dataSet = AsposeTool.fillNestedListData((List) value, key); doc.getMailMerge().executeWithRegions(dataSet); }else if(value instanceof byte[]){ builder.moveToMergeField(key); builder.insertImage((byte[]) value); }else if(value instanceof String){ String valStr = (String) value; if(AsposeTool.BOX_CHECKED.equals(valStr)){ builder.moveToMergeField(key); //设置字体 builder.getFont().setName("Wingdings 2"); builder.write("\uF052"); }else if(AsposeTool.BOX_UNCHECKED.equals(valStr)){ builder.moveToMergeField(key); //设置字体 builder.getFont().setName("Wingdings 2"); builder.write("\uF0A3"); }else if(AsposeTool.RADIO_CHECKED.equals(valStr)){ builder.moveToMergeField(key); //设置字体 builder.getFont().setName("Wingdings 2"); builder.write("\uF09B"); }else if(AsposeTool.RADIO_UNCHECKED.equals(valStr)){ builder.moveToMergeField(key); //设置字体 builder.getFont().setName("Wingdings 2"); builder.write("\uF099"); }else{ textMap.put(key,valStr); } }else{ textMap.put(key,value); } } String[] textKeyArr = textMap.keySet().stream().toArray(String[]::new); List<Object> textValList = new ArrayList(); for (String key : textKeyArr) { textValList.add(textMap.get(key)); } Object[] textValArr = textValList.stream().toArray(Object[]::new); doc.getMailMerge().execute(textKeyArr,textValArr); int saveFormat = "docx".equalsIgnoreCase(FileUtil.extName(resourceClassPath)) ? SaveFormat.DOCX : SaveFormat.DOC; doc.save(bout, saveFormat); int pageCount = doc.getPageCount(); return new Pair(bout.toByteArray(),pageCount); } }}
3 示例
本示例中 展示了各种填充类型:单个变量的替换 照片 单选框 复选框 表格中文字行循环 嵌套表格循环 带图片和文字的表格行循环
3.1 需要填充的模板
3.2 实现方法
/** * 根据模板导出word * @return */ @SneakyThrows @PostMapping("fillTemplate") public Ret fillTemnlate(){ byte[] photoBytes = null; byte[] netBytes = null; byte[] fireBytes = null; try( ByteArrayOutputStream bos1 = new ByteArrayOutputStream(); ByteArrayOutputStream bos2 = new ByteArrayOutputStream(); ByteArrayOutputStream bos3 = new ByteArrayOutputStream(); ){ Thumbnails.of("C:/Users/Administrator/Desktop/aspose/红孩儿.jpg").forceSize(60,100).toOutputStream(bos1); photoBytes = bos1.toByteArray(); Thumbnails.of("C:/Users/Administrator/Desktop/aspose/蜘蛛网.jpg").forceSize(100,100).toOutputStream(bos2); netBytes = bos2.toByteArray(); Thumbnails.of("C:/Users/Administrator/Desktop/aspose/喷火.jpg").forceSize(100,100).toOutputStream(bos3); fireBytes = bos3.toByteArray(); } //准备导出的数据 PersonInfo personInfo = new PersonInfo().setName("红孩儿").setIdNo("123").setTel("666").setAddr("火焰山").setTopEdu("小学") .setRadioMale(AsposeTool.RADIO_CHECKED).setRadioFemale(AsposeTool.RADIO_UNCHECKED) .setCheckboxSwim(AsposeTool.BOX_CHECKED).setCheckboxRun(AsposeTool.BOX_CHECKED).setCheckboxJump(AsposeTool.BOX_UNCHECKED) .setPersonPhoto(photoBytes); ArrayList<EduExpe> eduList = new ArrayList<>(); eduList.add(new EduExpe().setStartTime("2020年9月").setEndTime("2021年7月").setSchool("盘丝洞幼儿园").setMajor("织网")); eduList.add(new EduExpe().setStartTime("2021年9月").setEndTime("2022年7月").setSchool("火焰山小学").setMajor("喷火")); ArrayList<HisScore> hisScoreList = new ArrayList<>(); ArrayList<SubjectScore> subjectScoreList_A = new ArrayList<>(); subjectScoreList_A.add(new SubjectScore().setYear("2020").setScore("80")); subjectScoreList_A.add(new SubjectScore().setYear("2021").setScore("90")); ArrayList<SubjectScore> subjectScoreList_B = new ArrayList<>(); subjectScoreList_B.add(new SubjectScore().setYear("2021").setScore("85")); subjectScoreList_B.add(new SubjectScore().setYear("2022").setScore("95")); hisScoreList.add(new HisScore().setSubject("吐丝").setSubjectScoreList(subjectScoreList_A)); hisScoreList.add(new HisScore().setSubject("点火").setSubjectScoreList(subjectScoreList_B)); LinkedList<PersonPaper> paperList = new LinkedList<>(); paperList.add(new PersonPaper().setGainTime("2021年").setPaperName("织网小能手").setPaperPhoto(netBytes)); paperList.add(new PersonPaper().setGainTime("2022年").setPaperName("喷火大王").setPaperPhoto(fireBytes)); LinkedHashMap<String,Object> dataMap = new LinkedHashMap<>(); Map<String, Object> personInfoMap = BeanUtil.beanToMap(personInfo); dataMap.putAll(personInfoMap); dataMap.put("eduList",eduList); dataMap.put("paperList",paperList); dataMap.put("hisScoreList",hisScoreList); //对于使用复制行 来完成数据迭代插入的情况 必须确定需要复制的行在表格的最后一行 Map<String,Integer> cloneKeyTableIndexMap = new HashMap<>(); cloneKeyTableIndexMap.put("paperList",0); //导入 Pair<byte[], Integer> resultPair = AsposeTool.fillTpl(dataMap, "templates/complexTable.docx",cloneKeyTableIndexMap); byte[] bytes = resultPair.getKey(); //接口直接返回文件的话 直接使用bytes就行 方法返回类型使用ResponseEntity<byte[]> //以下是保存文件的方法 InputStream inputStream = new ByteArrayInputStream(bytes); Document doc = new Document(inputStream); doc.save("C:/Users/Administrator/Desktop/aspose/模板填充.docx", SaveFormat.DOCX); return Ret.success(); }
可以看到 封装后 实现导入起始很简单了 只需要按照入参规则准备好需要导入datamap 一步就可以实现填充
3.3 实现效果
基本上到此 一般情况下 各种填充的需求都可以满足了
还有特别变态的需求 要多个文档合并 加目录 加页码 或者非标准目录(手动生成目录的) 请自己结合各种提到的方法去实现吧 有疑问可以留言讨论
aspose常规使用 就到此结束了
当然他还包含了各种生成word方法的封装 一般也不怎么常用 就不再讨论了有实际需求的 可以自行了解下
以下是官方demo 包含了各种例子 挺全的
GitHub - aspose-words/Aspose.Words-for-Java: Aspose.Words for Java examples, plugins and showcases
以下是API文档
Document |Aspose.Words for Java
不能直接看源码 源码都是被加密混淆过的 但是基于以上2个文档 基本就可以整明白所需的功能了