个人博客网:https://wushaopei.github.io/ (你想要这里多有)
最近公司的需求大多都需要用到报表的导入导出,以及模板数据的填充操作,因此,将最近所使用的一些报表用过的功能从项目中拆分出来,做一个笔记。
1、创建继承 - XWPFDocument 类的 子类,创建对应的document 用于调用POI方法进行数据替换;
需求本身是往模板中写入数据,但由于模板本身表格的不规则,以及写入数据项表格是非空的,无法使用插入操作,所以需使用替换功能进行数据插入,效果相对直接在空白处插入数据,更具有针对性;
package com.example.poiutis.utils.XWPFDocuments;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlToken;
import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps;
import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D;
import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline;
import java.io.IOException;
import java.io.InputStream;
public class CustomXWPFDocument extends XWPFDocument {
public CustomXWPFDocument() {
super();
}
public CustomXWPFDocument(OPCPackage opcPackage) throws IOException {
super(opcPackage);
}
public CustomXWPFDocument(InputStream in) throws IOException {
super(in);
}
public void createPicture(XWPFRun run, String blipId, int id, int width, int height) {
final int EMU = 9525;
width *= EMU;
height *= EMU;
//旧版本方法 .getPackageRelationship() 在该依赖包下已被删除
//String blipId = getAllPictures().get(id).getPackageRelationship().getId();
//在docment下创建XWPFRun 图片会被添加到文档末尾
// CTInline inline = createParagraph().createRun().getCTR().addNewDrawing().addNewInline();
CTInline inline =run.getCTR().addNewDrawing().addNewInline();
String picXml = "" +
"" +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" " +
" ";
//CTGraphicalObjectData graphicData = inline.addNewGraphic().addNewGraphicData();
XmlToken xmlToken = null;
try {
xmlToken = XmlToken.Factory.parse(picXml);
} catch (XmlException xe) {
xe.printStackTrace();
}
inline.set(xmlToken);
//graphicData.set(xmlToken);
inline.setDistT(0);
inline.setDistB(0);
inline.setDistL(0);
inline.setDistR(0);
CTPositiveSize2D extent = inline.addNewExtent();
extent.setCx(width);
extent.setCy(height);
CTNonVisualDrawingProps docPr = inline.addNewDocPr();
docPr.setId(id);
docPr.setName("Picture " + id);
docPr.setDescr("Generated");
}
}
2、替换数据的类
package com.example.poiutis.utils.XWPFDocuments;
import org.apache.poi.POIXMLDocument;
import org.apache.poi.xwpf.usermodel.*;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class POIWordUtil{
/**
* 根据模板生成新word文档
* 判断表格是需要替换还是需要插入,判断逻辑有$为替换,表格无$为插入
*
* @param inputUrl 模板存放地址
* @param outputUrl 新文档存放地址
* @param textMap 需要替换的信息集合
* @param tableList 需要插入的表格信息集合
* @return 成功返回true, 失败返回false
*/
public static boolean changWords(String inputUrl, String outputUrl,
Map textMap, List tableList, Integer size, HttpServletResponse response,String filename) {
//模板转换默认成功
boolean changeFlag = true;
try {
//获取docx解析对象
CustomXWPFDocument document = new CustomXWPFDocument(POIXMLDocument.openPackage(inputUrl));
//解析替换文本段落对象
POIWordUtil.changeText(document, textMap);
//解析替换表格对象
POIWordUtil.changeTable(document, textMap, tableList);
//生成新的word
File file = new File(outputUrl);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
//直接输出到临时目录下
FileOutputStream stream = new FileOutputStream(file);
document.write(stream);
stream.close();
// try {
// Thread.sleep(600L);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
} catch (IOException e) {
e.printStackTrace();
changeFlag = false;
}
return changeFlag;
}
public static boolean changWord(String inputUrl, String outputUrl,
Map textMap, List tableList, HttpServletResponse response,String filename) {
//模板转换默认成功
boolean changeFlag = true;
try {
//获取docx解析对象
CustomXWPFDocument document = new CustomXWPFDocument(POIXMLDocument.openPackage(inputUrl));
//解析替换文本段落对象
POIWordUtil.changeText(document, textMap);
//解析替换表格对象
POIWordUtil.changeTable(document, textMap, tableList);
String name=filename+".docx";
response.setContentType("octets/stream");
// 防止乱码
response.addHeader("Content-Disposition", "attachment;filename="
+ URLEncoder.encode(name, "UTF-8"));
// response.setHeader("Content-Disposition", "attachment; filename=" + new String((filename+".docx").getBytes("GB2312"),"ISO8859-1"));
// response.setContentType("application/x-msdownload;");
// response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
document.write(response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
changeFlag = false;
}
return changeFlag;
}
/**
* 替换段落文本
*
* @param document docx解析对象
* @param textMap 需要替换的信息集合
*/
public static void changeText(XWPFDocument document, Map textMap) {
//获取段落集合
List paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
//判断此段落时候需要进行替换
String text = paragraph.getText();
if (checkText(text)) {
List runs = paragraph.getRuns();
for (XWPFRun run : runs) {
//替换模板原来位置
run.setText((String) changeValue(run.toString(), textMap), 0);
}
}
}
}
/**
* 替换表格对象方法
*
* @param document docx解析对象
* @param textMap 需要替换的信息集合
* @param tableList 需要插入的表格信息集合
*/
public static void changeTable(CustomXWPFDocument document, Map textMap,
List tableList) {
//获取表格对象集合
List tables = document.getTables();
for (int i = 0; i < tables.size(); i++) {
//只处理行数大于等于2的表格,且不循环表头
XWPFTable table = tables.get(i);
if (table.getRows().size() > 1) {
//判断表格是需要替换还是需要插入,判断逻辑有$为替换,表格无$为插入
if (checkText(table.getText())) {
List rows = table.getRows();
//遍历表格,并替换模板
eachTable(document, rows, textMap);
} else {
// System.out.println("插入"+table.getText());
insertTable(table, tableList);
}
}
}
}
/**
* 遍历表格,包含对图片内容的替换
*
* @param rows 表格行对象
* @param textMap 需要替换的信息集合
*/
public static void eachTable(CustomXWPFDocument document, List rows, Map textMap) {
for (XWPFTableRow row : rows) {
List cells = row.getTableCells();
for (XWPFTableCell cell : cells) {
//判断单元格是否需要替换
if (checkText(cell.getText())) {
List paragraphs = cell.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
List runs = paragraph.getRuns();
for (XWPFRun run : runs) {
// run.setText(changeValue(run.toString(), textMap),0);
Object ob = changeValue(run.toString(), textMap);
if (ob instanceof String) {
run.setText((String) ob, 0);
} else if (ob instanceof Map) {
run.setText("", 0);
Map pic = (Map) ob;
int width = Integer.parseInt(pic.get("width").toString());
int height = Integer.parseInt(pic.get("height").toString());
int picType = getPictureType(pic.get("type").toString());
byte[] byteArray = (byte[]) pic.get("content");
ByteArrayInputStream byteInputStream = new ByteArrayInputStream(byteArray);
try {
String ind = document.addPictureData(byteInputStream, picType);
document.createPicture(run, ind, document.getNextPicNameNumber(picType), width, height);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
}
}
}
/**
* 为表格插入数据,行数不够添加新行
*
* @param table 需要插入数据的表格
* @param tableList 插入数据集合
*/
public static void insertTable(XWPFTable table, List tableList) {
//创建行,根据需要插入的数据添加新行,不处理表头
for (int i = 1; i < tableList.size(); i++) {
XWPFTableRow row = table.createRow();
}
//遍历表格插入数据
List rows = table.getRows();
for (int i = 1; i < rows.size(); i++) {
XWPFTableRow newRow = table.getRow(i);
List cells = newRow.getTableCells();
for (int j = 0; j < cells.size(); j++) {
XWPFTableCell cell = cells.get(j);
cell.setText(tableList.get(i - 1)[j]);
}
}
}
/**
* 判断文本中时候包含$
*
* @param text 文本
* @return 包含返回true, 不包含返回false
*/
public static boolean checkText(String text) {
boolean check = false;
if (text.indexOf("$") != -1) {
check = true;
}
return check;
}
/**
* 匹配传入信息集合与模板
*
* @param value 模板需要替换的区域
* @param textMap 传入信息集合
* @return 模板需要替换区域信息集合对应值
*/
public static Object changeValue(String value, Map textMap) {
Object obj = null;
Set> textSets = textMap.entrySet();
for (Map.Entry textSet : textSets) {
//匹配模板与替换值 格式${key}
String key = "${" + textSet.getKey() + "}";
if (value.indexOf(key) != -1) {
obj = textSet.getValue();
}
}
if (!(obj instanceof Map)) {
//模板未匹配到区域替换为空
if (obj != null && checkText(obj.toString())) {
obj = "";
}
}
return obj;
}
/**
* 根据图片类型,取得对应的图片类型代码
*
* @param picType
* @return int
*/
private static int getPictureType(String picType) {
int res = CustomXWPFDocument.PICTURE_TYPE_PICT;
if (picType != null) {
if (picType.equalsIgnoreCase("png")) {
res = CustomXWPFDocument.PICTURE_TYPE_PNG;
} else if (picType.equalsIgnoreCase("dib")) {
res = CustomXWPFDocument.PICTURE_TYPE_DIB;
} else if (picType.equalsIgnoreCase("emf")) {
res = CustomXWPFDocument.PICTURE_TYPE_EMF;
} else if (picType.equalsIgnoreCase("jpg") || picType.equalsIgnoreCase("jpeg")) {
res = CustomXWPFDocument.PICTURE_TYPE_JPEG;
} else if (picType.equalsIgnoreCase("wmf")) {
res = CustomXWPFDocument.PICTURE_TYPE_WMF;
}
}
return res;
}
/**
* 将输入流中的数据写入字节数组
*
* @param in
* @return
*/
public static byte[] inputStream2ByteArray(InputStream in, boolean isClose) {
byte[] byteArray = null;
try {
int total = in.available();
byteArray = new byte[total];
in.read(byteArray);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (isClose) {
try {
in.close();
} catch (Exception e2) {
System.out.println("关闭流失败");
}
}
}
return byteArray;
}
}
POIWordUtil中生成word后已默认导出到指定目录下,具体可参照完整代码;最底部会提供git链接
3、调用zip压缩的工具类读取指定路径下目录里的所有文件,添加到zip类中,以流的方式导出;
package com.example.poiutis.utils.XWPFDocuments;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* 解压和压缩工具
* @ClassName ZipExportUtils
* @Description TODO
* @Author wushaopei
* @classname ZipUtil
* @Date 2019/7/15 16:55
* @Version 1.0
*/
@Component
public class ZipExportUtils {
private static Logger logger = LoggerFactory.getLogger(ZipExportUtils.class);
private static String location;
@Value("${img.location}")
public void setLocation(String location){
ZipExportUtils.location = location;
}
/**
* 压缩文件
*
* @param sourceFilePath 待压缩文件夹
* @param zipFilePath 压缩文件存放路径
* @param fileName 压缩文件名
*/
public static boolean fileToZip(String sourceFilePath, String zipFilePath, String fileName,HttpServletResponse response) {
boolean flag = false;
File sourceFile = new File(sourceFilePath);
FileInputStream fis = null;
BufferedInputStream bis = null;
OutputStream fos = null;
ZipOutputStream zos = null;
if (!sourceFile.exists()) {
logger.info("待压缩的文件目录:" + sourceFilePath + "不存在.");
} else {
try {
File zipFile = new File(zipFilePath + File.separator + fileName + ".zip");
if (zipFile.exists()) {
logger.info(zipFilePath + "目录下存在名字为:" + fileName + ".zip" + "打包文件.");
}
File[] sourceFiles = sourceFile.listFiles();
if (null == sourceFiles || sourceFiles.length < 1) {
logger.info("待压缩的文件目录:" + sourceFilePath + "里面不存在文件,无需压缩.");
} else {
logger.info("开始压缩:");
// fos = new FileOutputStream(zipFile);
// zos = new ZipOutputStream(new BufferedOutputStream(fos));
fos =response.getOutputStream();
zos =new ZipOutputStream(fos);
/* response.setContentType("application/zip");
response.setHeader("Connection","close");//表示不能用浏览器直接打开
response.setHeader("Accept-Ranges","bytes");//告诉客户端允许端点续传多线程连接下载
response.setHeader("Content-Disposition",
"attachment;filename="+new String((fileName+".zip").getBytes("UTF-8"),"UTF-8"));
response.setCharacterEncoding("UTF-8");
response.setContentType("application/zip");*/
String name=fileName+".zip";
response.setContentType("octets/stream");
// 防止乱码
response.addHeader("Content-Disposition", "attachment;filename="
+ URLEncoder.encode(name, "UTF-8"));
byte[] bufs = new byte[1024 * 10];
for (int i = 0; i < sourceFiles.length; i++) {
//创建ZIP实体,并添加进压缩包
ZipEntry zipEntry = new ZipEntry(sourceFiles[i].getName());
zos.putNextEntry(zipEntry);
//读取待压缩的文件并写进压缩包里
fis = new FileInputStream(sourceFiles[i]);
bis = new BufferedInputStream(fis, 1024 * 10);
int read = 0;
while ((read = bis.read(bufs, 0, 1024 * 10)) != -1) {
zos.write(bufs, 0, read);
}
fis.close();
bis.close();
//删除文件
sourceFiles[i].delete();
}
flag = true;
}
} catch (FileNotFoundException e) {
logger.info("压缩失败,文件找不到");
throw new RuntimeException(e);
} catch (IOException e) {
logger.info("压缩失败,压缩输出异常");
throw new RuntimeException(e);
} finally {
//关闭流
try {
if (null != bis) {
bis.close();
}
if (null != zos) {
zos.close();
}
} catch (IOException e) {
logger.info("关闭流失败");
throw new RuntimeException(e);
}
}
}
return flag;
}
/**解压zip文件
* @param folderLocationPath 待解压的文件
* @param unZipFolderPath 解压后的文件夹
* @return 返回解压后的文件绝对路径
*/
public static String unZip(String folderLocationPath,String unZipFolderPath) {
try {
ZipFile zip = new ZipFile(folderLocationPath, Charset.forName("GBK"));
String decompressionPath = unZipFolderPath;
File pathFile = new File(decompressionPath);
if (!pathFile.exists()) {
pathFile.mkdirs();
}else {
if(pathFile.listFiles().length!=0){
for(File file : pathFile.listFiles()){
file.delete();
}
}
}
logger.info("开始解压");
for (Enumeration extends ZipEntry> entries = zip.entries(); entries.hasMoreElements(); ) {
ZipEntry entry = (ZipEntry) entries.nextElement();
String zipEntryName = entry.getName();
logger.info("待解压文件:" + zipEntryName);
InputStream in = zip.getInputStream(entry);
String outPath = (decompressionPath + File.separator + zipEntryName).replaceAll("\\*", "/");
//判断文件全路径是否为文件夹,如果是上面已经上传,不需要解压
if (new File(outPath).isDirectory()) {
continue;
}
//输出文件路径信息
logger.info("解压输出路径:" + outPath);
OutputStream out = new FileOutputStream(outPath);
byte[] buf1 = new byte[1024];
int len;
while ((len = in.read(buf1)) > 0) {
out.write(buf1, 0, len);
}
in.close();
out.close();
}
return decompressionPath;
} catch (Exception e) {
e.printStackTrace();
logger.error("解压失败");
return null;
}
}
}
这里要注意一个问题:就是响应头乱码的问题,一定要使用 "UTF-8"进行编码,在前后端交互执行下载操作的情况下,前端是通过响应头编码完成的文件名来生成二进制流文件的文件名的,所以要注意查看接口的响应头文件名是否发生乱码问题。
完整的有接口功能代码 GitHub 链接地址: https://github.com/wushaopei/SpringBoot_POIUtils