前言
相信不少同学在开发中都会遇到导出Word这种需求,今天将Word的导出工具和大家进行一个分享,如有错误还请大佬们批评指正。该工具类可以实现线上导出Word,或者生成的Word文件保存到本地目录,实现了多页合并导出。废话不多,上代码——
一、这是在maven项目中,首先需要引入依赖
<!--引入POI--> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.9</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.9</version> </dependency> <!--引入coobird--> <dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId> <version>0.4.11</version> </dependency><!--引入freemarker--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency><!--引入io流--> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.3</version> </dependency>
二、 创建Word导出工具类
工具类中有从网络上下载图片保存到本地的方法,也有直接调用浏览器下载的方法,根据自己的需求修改即可。(有些方法注释了,需要打开注释就行)
package com.xxx.xxx.microsoftOffice;
import freemarker.template.*;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.io.IOUtils;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.*;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import org.apache.commons.codec.binary.Base64;
import static org.apache.poi.util.IOUtils.copy;
/**
* 导出Word
* @author Ldpy
*
* @param <>
*/
public class ExportWordUtil {
private Logger log = Logger.getLogger(ExportWordUtil.class.toString());
private Configuration config = null;
public ExportWordUtil() {
config = new Configuration();
config.setDefaultEncoding("utf-8");
}
/**
* FreeMarker生成Word
* @param dataMap 数据
* @param templateName 目标名
* @param saveFilePath 保存文件路径的全路径名(路径+文件名)
*/
public void createWord(Map<String, Object> dataMap, String templateName, String saveFilePath, HttpServletResponse response) {
//加载模板(路径)数据
config.setClassForTemplateLoading(this.getClass(), "/templates");
//设置异常处理器 这样的话 即使没有属性也不会出错 如:${list.fullName}...不会报错
config.setTemplateExceptionHandler(TemplateExceptionHandler.IGNORE_HANDLER);
Template template = null;
String content = null;
if(templateName.endsWith(".ftl")) {
templateName = templateName.substring(0, templateName.indexOf(".ftl"));
}
try {
template = config.getTemplate(templateName + ".ftl");
content = FreeMarkerTemplateUtils.processTemplateIntoString(template, dataMap);
} catch (TemplateNotFoundException e) {
log.info("模板文件未找到");
e.printStackTrace();
} catch (MalformedTemplateNameException e) {
log.info("模板类型不正确");
e.printStackTrace();
} catch (TemplateException e) {
log.info("用模板生成文件失败");
e.printStackTrace();
} catch (IOException e) {
log.info("IO读取失败");
e.printStackTrace();
}
// 调用浏览器下载
downloadWord(response, content);
// 输出到本地
/*File outFile = new File(saveFilePath);
if(!outFile.getParentFile().exists()) {
outFile.getParentFile().mkdirs();
}
Writer out = null;
FileOutputStream fos = null;
try {
fos = new FileOutputStream(outFile);
} catch (FileNotFoundException e) {
log.info("输出文件时未找到文件");
e.printStackTrace();
}
out = new BufferedWriter(new OutputStreamWriter(fos));
//将模板中的预先的代码替换为数据
try {
template.process(dataMap, out);
} catch (TemplateException e) {
log.info("填充模板时异常");
e.printStackTrace();
} catch (IOException e) {
log.info("IO读取时异常");
e.printStackTrace();
}
log.info("由模板文件:" + templateName + ".ftl" + " 生成文件 :" + saveFilePath + " 成功!!");
try {
out.close();//web项目不可关闭
} catch (IOException e) {
log.info("关闭Write对象出错");
e.printStackTrace();
}*/
}
/**
* 获得图片的Base64编码和大小
* @param imgUrl
* @return
*/
public Map<String, Object> getImageStr(String imgUrl) {
// 读取网络图片的方式
try {
// 构造URL
URL url = new URL(imgUrl);
// 打开连接
URLConnection con = url.openConnection();
// 设置请求超时为10s
con.setConnectTimeout(10*1000);
// 输入流
InputStream in = null;
try {
in = con.getInputStream();
} catch (FileNotFoundException e) {
log.info("加载图片未找到");
e.printStackTrace();
}
// 压缩图片
/*ByteArrayOutputStream output = new ByteArrayOutputStream();
copy(in, output);
byte[] src = output.toByteArray();
byte[] data = compressPhotoByQuality(src, 0.3F, 30);
// 定义输入流缓存
ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
infoStream.write(data);*/
// 2K的数据缓冲
byte[] bs = new byte[2048];
// 读取到的数据长度
int len;
// 定义输入流缓存
ByteArrayOutputStream infoStream = new ByteArrayOutputStream();
// 开始读取
while ((len = in.read(bs)) != -1) {
// 将缓冲区中读取的input数据写入infoStream
infoStream.write(bs, 0, len);
}
// 获取的图片的大小
/*InputStream inputStream = new ByteArrayInputStream(infoStream.toByteArray());
BufferedImage sourceImg = ImageIO.read(inputStream);
int width = sourceImg.getWidth();
int height = sourceImg.getHeight();*/
BASE64Encoder encoder = new BASE64Encoder();
String result = encoder.encode(infoStream.toByteArray());
// 清空缓存
infoStream.reset();
// 关闭链接
infoStream.close();
// 完毕,关闭链接
in.close();
Map<String, Object> map = new HashMap<>(8);
map.put("data", result);
/*map.put("width", 212); // 宽度固定
map.put("height", (new BigDecimal("212").divide(new BigDecimal(String.valueOf(width)), 10,
BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(String.valueOf(height)))).
setScale(1, BigDecimal.ROUND_HALF_UP));*/ // 高度保持纵横比
return map;
}catch (Exception e) {
e.printStackTrace();
}
return null;
// 输入流(读取本地图片的方式)
/*InputStream in = new FileInputStream(imgUrl);
byte[] data = null;
try {
data = new byte[in.available()];
//注:FileInputStream.available()方法可以从输入流中阻断由下一个方法调用这个输入流中读取的剩余字节数
in.read(data);
} catch (IOException e) {
log.info("IO操作图片错误");
e.printStackTrace();
}finally {
in.close();
}
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(data);*/
}
/**
* 获得图片存入本地
* @param imgUrl
* @return
*/
public void saveImage(String imgUrl) throws IOException {
// 构造URL
URL url = new URL(imgUrl);
// 打开连接
URLConnection con = url.openConnection();
// 设置请求超时为10s
con.setConnectTimeout(10*1000);
// 输入流
InputStream in = null;
try {
in = con.getInputStream();
} catch (FileNotFoundException e) {
log.info("加载图片未找到");
e.printStackTrace();
}
// 2K的数据缓冲
byte[] bs = new byte[2048];
// 读取到的数据长度
int len;
// 输出的文件流
File sf=new File("G:\\新建文件夹");
if(!sf.exists()){
sf.mkdirs();
}
// 获取图片的扩展名
String extensionName = imgUrl.substring(imgUrl.lastIndexOf(".") + 1);
// 新的图片文件名 = 编号 +"."图片扩展名
String newFileName = System.currentTimeMillis() + "." + extensionName;
OutputStream os = new FileOutputStream(sf.getPath()+"\\"+newFileName);
// 开始读取
while ((len = in.read(bs)) != -1) {
os.write(bs, 0, len);
}
// 完毕,关闭所有链接
os.close();
in.close();
}
// 浏览器下载
private void downloadWord(HttpServletResponse response, String content){
// 文件名
String fileName = "物联网卫星定位终端监控设备加装信息";
// 创建输入流读取
InputStream inputStream = null;
// 创建输出流
ServletOutputStream out = null;
try{
inputStream = IOUtils.toInputStream(content, "utf-8");
// 设置response的编码方式
response.setHeader("content-type", "application/octet-stream");
response.setContentType("application/octet-stream;charset=UTF-8");
// 设置文件名
response.setHeader("Content-Disposition", "attachment;filename=".concat(String.valueOf(URLEncoder.encode(fileName, "UTF-8"))+ ".doc"));
// 输出流赋值
out = response.getOutputStream();
// 设置缓冲区
byte[] buffer = new byte[10240];
int bytesToRead = -1;
// 通过循环将读入的Word文件的内容输出到浏览器中
while((bytesToRead = inputStream.read(buffer)) != -1) {
out.write(buffer, 0, bytesToRead);
}
}catch (Exception e){
e.printStackTrace();
throw new RuntimeException("导出word失败!,请联管理人员");
}finally {
try {
if (out != null)
out.close();
if (inputStream != null)
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 递归压缩图片
* @param bytes 图片字节数组
* @param quality 压缩比例
* @param maxkb 压缩后最大值
* @return
* @throws IOException
*/
public static byte[] compressPhotoByQuality(byte[] bytes,Float quality,long maxkb) throws IOException{
if(bytes == null){
return bytes;
}
//log.info("开始按质量压缩图片({}kb)。",bytes.length/1024);
// 如果配置的>=1,则不再处理,多说无益
if(quality >= 1){
//log.info("quality>=1,不执行压缩。");
return bytes;
}
// 满足目标kb值,则返回
long fileSize = bytes.length;
if (fileSize <= maxkb * 1024) {
//log.info("图片文件{}kb<={}kb,不再压缩质量。",fileSize/1024,maxkb);
return bytes;
}
// Closing a <tt>ByteArrayOutputStream</tt> has no effect. 因此无需close
ByteArrayOutputStream out = null;
out = new ByteArrayOutputStream();
BufferedImage bim = ImageIO.read(new ByteArrayInputStream(bytes));
int imgWidth = bim.getWidth();
int imgHeight = bim.getHeight();
// 如果不处理size,只用quality,可能导致一致压缩不到目标值,一致递归在当前方法中!!
int desWidth = new BigDecimal(imgWidth).multiply(new BigDecimal(quality)).intValue();
int desHeight = new BigDecimal(imgHeight).multiply(new BigDecimal(quality)).intValue();
//log.info("图片文将按照width={}*height={}进行压缩,画质quality={}。",desWidth,desHeight,quality);
Thumbnails.of(new ByteArrayInputStream(bytes)).size(desWidth, desHeight).outputQuality(quality).outputFormat("jpg").toOutputStream(out);
//递归
return compressPhotoByQuality(out.toByteArray(), quality, maxkb);
}
}
三、word模板制作
在制作模板时,用Microsoft Word(office word)的版本来制作,不要用wps。先创建一个自己需要的Word文档,包括字体样式,表格样式,图片样式等,反正就是最后自己想导出的Word样式。例如想导出如下图样式的word文档,一定先把想要的格式弄好。
之后另存为 Word XML 文档 (*.xml) 类型格式,先保存一份正常的 Word 文档 (*.docx) ,便于之后查找需要替换的单词。
四、修改ftl文件
将刚才保存的xml文件重命名,将其类型修改为ftl。我的是将WordTemplate.xml改为WordTemplate.ftl。然后复制到项目的resources下(根据自己实际项目选择存放路径),用IDEA打开,如下图。
把红圈中自己创建模板时填的单词替换成程序中实际返回的字段名,如将“所有人姓名”定义的“11111111111111111111111111”改为程序中的字段名${ownerName},将所有需要程序返回的值都用${}包起来,图片也一样,只不过图片生成文件时是base64格式的,如下图
把黄色框内<pkg:binaryData></pkg:binaryData>标签或者<w:binData></w:binData>中的图片base64格式改为程序中的字段名(不同样式标签不同),如${picture},都改好后保存文件。
五、编写应用代码
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/**
* @return
* @Author Ldpy
* @Description 批量导出word文件
* @Date 2021/7/9 17:02
* @Param
**/
@RequestMapping("exportPrintWord")
public void exportPrintPage(HttpServletResponse response) {
List<DtIotNjMonitorInfoDTOForBJ> list = dtIotNjMonitorInfoService.printPage();
Calendar cal = Calendar.getInstance();
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1;
int day = cal.get(Calendar.DATE);
ExportWordUtil ewu = new ExportWordUtil();
Map<String, Object> dataMap = new HashMap<>();
List<Map<String, Object>> resultList = new ArrayList<>();
dataMap.put("yearStr", String.valueOf(year));
dataMap.put("dateStr", year + "年" + month + "月" + day + "日");
dataMap.put("resultList", resultList);
// 默认图片
String defaut = "iVBORw0KGgoAAAANSUhEUgAAAKoAAADkCAYAAAAIJL+eAAAAAXNSR0IArs4c6QAAAARnQU1BAACx" +
"jwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAJ5SURBVHhe7dJBEcAwDMCwbPxxhkb3GYj6TvqY" +
"gJ/dPQOXe//C1YxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJ" +
"MCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBU" +
"EoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQY" +
"lQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJ" +
"RiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxK" +
"glFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSj" +
"kmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXB" +
"qCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJ" +
"MCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBU" +
"EoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglFJMCoJRiXBqCQY" +
"lQSjkmBUEoxKglFJMCoJRiXBqCQYlQSjkmBUEoxKglEJmPkACvoFedfRZ9kAAAAASUVORK5CYII=";
for (DtIotNjMonitorInfoDTOForBJ entity : list) {
Map<String, Object> map = new HashMap<>(32);
map.put("fullName", StringUtils.isNotBlank(entity.getFullName()) ? entity.getFullName() : "--");
map.put("mobile", StringUtils.isNotBlank(entity.getMobile()) ? entity.getMobile() : "--");
map.put("ownerAddress", StringUtils.isNotBlank(entity.getAddres()) ? entity.getAddres() : "--");
map.put("licenseNumber", StringUtils.isNotBlank(entity.getLicenseNumber()) ? entity.getLicenseNumber() : "--");
map.put("njFactoryName", StringUtils.isNotBlank(entity.getNjFactoryName()) ? entity.getNjFactoryName() : "--");
map.put("catThreeName", StringUtils.isNotBlank(entity.getCatThreeName()) ? entity.getCatThreeName() : "--");
map.put("njModel", StringUtils.isNotBlank(entity.getNjModel()) ? entity.getNjModel() : "--");
map.put("imei", StringUtils.isNotBlank(entity.getImei()) ? entity.getImei() : "--");
map.put("cTime", StringUtils.isNotBlank(entity.getCTime()) ? entity.getCTime() : "--");
try {
ExportWordUtil util = new ExportWordUtil();
if (StringUtils.isNotBlank(entity.getNjPicUrl())) {
Map map1 = util.getImageStr(entity.getNjPicUrl());
map.put("njPic", map1.get("data"));
} else {
map.put("njPic", defaut);
}
if (StringUtils.isNotBlank(entity.getNameplatePicUrl())) {
Map map1 = util.getImageStr(entity.getNameplatePicUrl());
map.put("nameplatePic", map1.get("data"));
} else {
map.put("nameplatePic", defaut);
}
if (StringUtils.isNotBlank(entity.getReductionPicUrl())) {
Map map1 = util.getImageStr(entity.getReductionPicUrl());
map.put("reductionPic", map1.get("data"));
} else {
map.put("reductionPic", defaut);
}
if (StringUtils.isNotBlank(entity.getTerminalBeforePicUrl())) {
Map map1 = util.getImageStr(entity.getTerminalBeforePicUrl());
map.put("terminalBeforePic", map1.get("data"));
} else {
map.put("terminalBeforePic", defaut);
}
if (StringUtils.isNotBlank(entity.getTerminalAfterPicUrl())) {
Map map1 = util.getImageStr(entity.getTerminalAfterPicUrl());
map.put("terminalAfterPic", map1.get("data"));
} else {
map.put("terminalAfterPic", defaut);
}
} catch (Exception e) {
e.printStackTrace();
}
resultList.add(map);
}
ewu.createWord(dataMap, "WordTemplate.ftl", null, response);
}
注意:样式不同,ftl文件中的标签会不同,学习的是方法,不能教条。我的需求是返回一个集合,即需要导出一批word数据,因此有类似需求的同学可以修改ftl文件,在标签<w:tbl>前面加入<#list resultList as item>,resultList是程序中定义的集合名,然后集合中的字段相应的改为${item.ownerName},${item.picture}等等,只要是循环都可以用<#list resultList as item>来解决,但一定要注意关闭标签:</w:tbl></#list>,我是在标签<w:tbl>前面加入<#list resultList as item>,所以在</w:tbl>之后用</#list>关闭标签。
如果在表格中需要有多张照片,需要在<w:binData w:name="${"wordml://02000003"+item_index+1+".jpg"}"标签中做类似的修改,即在默认生成的照片名后加“item_index+1”,这个标签<v:imagedata src="${"wordml://02000001"+item_index+1+".jpg"}"也需要做相同的修改,如下图