代码
依赖
<dependencies>
<!-- easypoi -->
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-base</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-web</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>cn.afterturn</groupId>
<artifactId>easypoi-annotation</artifactId>
<version>4.3.0</version>
</dependency>
<!-- pdf -->
<dependency>
<groupId>org.docx4j</groupId>
<artifactId>docx4j-export-fo</artifactId>
<version>6.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
</dependencies>
自定义注解
用于定义需要到处的实体类的属性信息
package com.zdd.export;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author zdd
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExportFiled {
String name() default "";
String type() default "string";
int height() default 200;
int width() default 500;
String format() default "yyy-MM-dd HH:mm:ss";
String replace() default "";
}
属性类型定义类
package com.zdd.export;
/**
* @description:
* @author: zdd
* @time: 2021/5/10 11:26
*/
public class FiledTypeConstant {
public final static String STRING = "string";
public final static String IMAGE = "image";
public final static String DATE = "date";
public final static String LIST = "list";
public final static String IMAGE_LIST = "imageList";
public final static String OBJECT = "object";
public final static String MAP = "map";
}
测试实体类
/**
* @description:
* @author: zdd
* @time: 2021/5/10 10:17
*/
@Data
public class Model {
@ExportFiled
private String name;
@ExportFiled(replace = "1_男,0_女")
private Integer gender;
@ExportFiled
private Integer age;
@ExportFiled(type = "image", width = 100, height = 50)
private String imagePath;
@ExportFiled(type = "date", format = "yyy-MM-dd HH:mm:ss")
private LocalDateTime date;
@ExportFiled(type = "list")
private List<Item> list;
@ExportFiled(type = "map")
private Map<String, Object> imageTest;
}
/**
* @description:
* @author: zdd
* @time: 2021/5/20 11:08
*/
@Data
public class ImageTest {
@ExportFiled
private String name;
@ExportFiled(type = "image")
private String mediaPath;
}
/**
* @description:
* @author: zdd
* @time: 2021/5/18 13:50
*/
@Data
public class Item {
@ExportFiled
private String name;
@ExportFiled
private Integer age;
@ExportFiled(type = "date", format = "yyy-MM-dd HH:mm:ss")
private LocalDateTime date;
}
导出工具类
package com.zdd.export.util;
import cn.afterturn.easypoi.entity.ImageEntity;
import cn.afterturn.easypoi.word.WordExportUtil;
import com.zdd.export.ExportFiled;
import com.zdd.export.FiledTypeConstant;
import com.zdd.export.Item;
import com.zdd.export.Model;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.docx4j.Docx4J;
import org.docx4j.convert.out.FOSettings;
import org.docx4j.fonts.IdentityPlusMapper;
import org.docx4j.fonts.Mapper;
import org.docx4j.fonts.PhysicalFonts;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @description:
* @author: zdd
* @time: 2021/5/10 9:09
*/
@Slf4j
public class PdfUtil<T> {
private static String separator = File.separator;//文件夹路径分格符
public static void main(String[] args) {
Model model = new Model();
model.setName("zdd");
model.setAge(111);
// model.setImagePath("https://img-home.csdnimg.cn/images/20201124032511.png");
model.setImagePath("http://129.168.2.117:9000/citymanage/Rec/2021/4/1/11399/IMG_20210401_081602.jpg");
model.setDate(LocalDateTime.now());
ArrayList<Item> items = new ArrayList<>();
Item item = new Item();
item.setName("ceshi");
item.setAge(123);
item.setDate(LocalDateTime.now());
item.setImagePath("https://img-home.csdnimg.cn/images/20201124032511.png");
items.add(item);
items.add(item);
model.setList(items);
// ArrayList<String> list = new ArrayList<>();
// list.add("https://img-home.csdnimg.cn/images/20201124032511.png");
// list.add("https://img-home.csdnimg.cn/images/20201124032511.png");
// model.setImageList(list);
// ArrayList<ImageTest> list = new ArrayList<>();
// list.add(new ImageTest(){{
// setName("zdd");
// setMediaPath("https://img-home.csdnimg.cn/images/20201124032511.png");
// }});
// list.add(new ImageTest(){{
// setName("ys");
// setMediaPath("https://img-home.csdnimg.cn/images/20201124032511.png");
// }});
// list.add(new ImageTest(){{
// setName("xdf");
// setMediaPath("https://img-home.csdnimg.cn/images/20201124032511.png");
// }});
// model.setImageList(list);
HashMap<String, Object> map = new HashMap<>();
ImageEntity imageEntity = new ImageEntity();
imageEntity.setUrl("https://img-home.csdnimg.cn/images/20201124032511.png");
imageEntity.setType(ImageEntity.URL);
imageEntity.setWidth(500);
imageEntity.setHeight(200);
map.put("name", "zdd");
map.put("mediaPath", imageEntity);
model.setImageTest(map);
PdfUtil<Model> pdfUtil = new PdfUtil<>();
String templatePath = PdfUtil.class.getClassLoader().getResource("templates/test.docx").getPath().replaceAll("%20", " ");
pdfUtil.exportPdf(model, templatePath, "C:\\Users\\zdd\\Desktop\\test.docx", "C:\\Users\\zdd\\Desktop\\out.pdf");
}
/**
* 导出pdf到Response请求中
* @param t
* @param templatePath
* @param fileName
* @param response
*/
public void exportPdf2Response(T t, String templatePath, String fileName, HttpServletResponse response){
//获取临时保存文件目录
String upfilePath = System.getProperty("java.io.tmpdir").concat("/smartcity/pdf/");
File upfilePathFile = new File(upfilePath);
templatePath = PdfUtil.class.getClassLoader().getResource("templates/" + templatePath).getPath().replaceAll("%20", " ");
if (!upfilePathFile.exists()){
upfilePathFile.mkdirs();
}
String pdfPathName = upfilePath.concat(fileName + ".pdf");
String wordPathName = upfilePath.concat(fileName + ".docx");
File outFile = new File(pdfPathName);
exportPdf(t, templatePath, wordPathName, pdfPathName);
try (
// 从response对象中得到输出流,准备下载
OutputStream myout = response.getOutputStream();
// 读出文件到i/o流
FileInputStream fis = new FileInputStream(outFile);
BufferedInputStream buf = new BufferedInputStream(fis);
){
// 设置response的编码方式
fileName = fileName + "pdf";
fileName = java.net.URLEncoder.encode(fileName, "UTF-8");
response.setHeader("Content-Disposition", "attachment; filename="+fileName);
response.setContentType("application/octet-stream;charset=utf-8");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
byte[] b = new byte[1024];// 相当于我们的缓存
long k = 0;// 该值用于计算当前实际下载了多少字节
// 开始循环下载
while (k < outFile.length()) {
int j = buf.read(b, 0, 1024);
k += j;
// 将b中的数据写到客户端的内存
myout.write(b, 0, j);
}
// 将写入到客户端的内存的数据,刷新到磁盘
myout.flush();
} catch (Exception e) {
log.error("导出PDF异常", e);
throw new RuntimeException("导出PDF异常");
}
}
/**
* 导出pdf到Response请求中
* @param t
* @param templatePath
* @param fileName
* @param response
*/
public void exportWord2Response(T t, String templatePath, String fileName, HttpServletResponse response){
//获取临时保存文件目录
String upfilePath = System.getProperty("java.io.tmpdir").concat("/smartcity/word/");
File upfilePathFile = new File(upfilePath);
templatePath = PdfUtil.class.getClassLoader().getResource("templates/" + templatePath).getPath().replaceAll("%20", " ");
if (!upfilePathFile.exists()){
upfilePathFile.mkdirs();
}
String wordPathName = upfilePath.concat(fileName + ".docx");
File outFile = new File(wordPathName);
exportWord(t, templatePath, wordPathName);
try (
// 从response对象中得到输出流,准备下载
OutputStream myout = response.getOutputStream();
// 读出文件到i/o流
FileInputStream fis = new FileInputStream(outFile);
BufferedInputStream buf = new BufferedInputStream(fis);
){
// 设置response的编码方式
fileName = fileName + ".docx";
fileName = java.net.URLEncoder.encode(fileName, "UTF-8");
response.setHeader("Content-Disposition", "attachment; filename="+fileName);
response.setContentType("application/octet-stream;charset=utf-8");
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
byte[] b = new byte[1024];// 相当于我们的缓存
long k = 0;// 该值用于计算当前实际下载了多少字节
// 开始循环下载
while (k < outFile.length()) {
int j = buf.read(b, 0, 1024);
k += j;
// 将b中的数据写到客户端的内存
myout.write(b, 0, j);
}
// 将写入到客户端的内存的数据,刷新到磁盘
myout.flush();
} catch (Exception e) {
log.error("导出Word异常", e);
throw new RuntimeException("导出Word异常");
}
}
/**
* 导出word和pdf到本地
* @param t
* @param templatePath
* @param wordPath
* @param pdfPath
*/
public void exportPdf(T t, String templatePath, String wordPath, String pdfPath){
Map<String, Object> map = class2Map(t);
FileOutputStream fos = null;
// templatePath = PdfUtil.class.getClassLoader().getResource("templates/" + templatePath).getPath().replaceAll("%20", " ");
try {
XWPFDocument doc = WordExportUtil.exportWord07(
templatePath, map);
fos = new FileOutputStream(wordPath);
doc.write(fos);
fos.flush();
convertDocx2Pdf(wordPath, pdfPath);
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if (fos != null){
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 导出word和pdf到本地
* @param t
* @param templatePath
* @param wordPath
*/
public void exportWord(T t, String templatePath, String wordPath){
Map<String, Object> map = class2Map(t);
FileOutputStream fos = null;
templatePath = PdfUtil.class.getClassLoader().getResource("templates/" + templatePath).getPath().replaceAll("%20", " ");
try {
XWPFDocument doc = WordExportUtil.exportWord07(
templatePath, map);
fos = new FileOutputStream(wordPath);
doc.write(fos);
fos.flush();
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
if (fos != null){
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public Map<String, Object> class2Map(T t){
HashMap<String, Object> map = new HashMap<>();
Class<?> tClass = t.getClass();
Field[] declaredFields = tClass.getDeclaredFields();
for (Field declaredField : declaredFields) {
ExportFiled annotation = declaredField.getAnnotation(ExportFiled.class);
if (annotation != null){
String type = annotation.type();
String key = "".equals(annotation.name()) ? declaredField.getName() : annotation.name();
declaredField.setAccessible(true);
try {
String value = declaredField.get(t) == null ? "" : declaredField.get(t).toString();
switch (type){
case FiledTypeConstant.IMAGE:
if (!"".equals(value)){
ImageEntity image = new ImageEntity();
image.setHeight(annotation.height());
image.setWidth(annotation.width());
image.setUrl(value);
image.setType(ImageEntity.URL);
map.put(key, image);
}else {
map.put(key, "");
}
break;
case FiledTypeConstant.DATE:
LocalDateTime date = (LocalDateTime) declaredField.get(t);
if (date != null){
DateTimeFormatter dfDate = DateTimeFormatter.ofPattern(annotation.format());
value = dfDate.format(date);
}
map.put(key, value);
break;
case FiledTypeConstant.LIST:
List list = (List) declaredField.get(t);
ArrayList<Map<String, Object>> maps = new ArrayList<>();
for (Object filed : list) {
PdfUtil<Object> objectPdfUtil = new PdfUtil<>();
Map<String, Object> filedMap = objectPdfUtil.class2Map(filed);
maps.add(filedMap);
}
map.put(key, maps);
break;
case FiledTypeConstant.IMAGE_LIST:
List<String> imageList = (List<String>) declaredField.get(t);
ArrayList<ImageEntity> imageEntityList = new ArrayList<>();
for (String s : imageList) {
ImageEntity imageEntity = new ImageEntity();
imageEntity.setHeight(annotation.height());
imageEntity.setWidth(annotation.width());
imageEntity.setUrl(s);
imageEntity.setType(ImageEntity.URL);
imageEntityList.add(imageEntity);
}
map.put(key, imageEntityList);
break;
case FiledTypeConstant.OBJECT:
Object o = declaredField.get(t);
PdfUtil<Object> objectPdfUtil = new PdfUtil<>();
Map<String, Object> filedMap = objectPdfUtil.class2Map(o);
map.put(key, filedMap);
break;
case FiledTypeConstant.MAP:
map.put(key, declaredField.get(t));
break;
default:
String replace = annotation.replace();
if (!"".equals(replace)){
String[] replaceList = replace.split(",");
for (String s : replaceList) {
String[] replaces = s.split("_");
if (value.equals(replaces[0])){
value = replaces[1];
break;
}
}
}
map.put(key, value);
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
return map;
}
/**
* word(docx)转pdf
* @param wordPath docx文件路径
* @return 生成的pdf路径
*/
public void convertDocx2Pdf(String wordPath, String pdfPath) {
OutputStream os = null;
InputStream is = null;
try {
is = new FileInputStream(new File(wordPath));
WordprocessingMLPackage mlPackage = WordprocessingMLPackage.load(is);
Mapper fontMapper = new IdentityPlusMapper();
fontMapper.put("隶书", PhysicalFonts.get("LiSu"));
fontMapper.put("宋体", PhysicalFonts.get("SimSun"));
fontMapper.put("微软雅黑", PhysicalFonts.get("Microsoft Yahei"));
fontMapper.put("黑体", PhysicalFonts.get("SimHei"));
fontMapper.put("楷体", PhysicalFonts.get("KaiTi"));
fontMapper.put("新宋体", PhysicalFonts.get("NSimSun"));
fontMapper.put("华文行楷", PhysicalFonts.get("STXingkai"));
fontMapper.put("华文仿宋", PhysicalFonts.get("STFangsong"));
fontMapper.put("宋体扩展", PhysicalFonts.get("simsun-extB"));
fontMapper.put("仿宋", PhysicalFonts.get("FangSong"));
fontMapper.put("仿宋_GB2312", PhysicalFonts.get("FangSong_GB2312"));
fontMapper.put("幼圆", PhysicalFonts.get("YouYuan"));
fontMapper.put("华文宋体", PhysicalFonts.get("STSong"));
fontMapper.put("华文中宋", PhysicalFonts.get("STZhongsong"));
mlPackage.setFontMapper(fontMapper);
//输出pdf文件路径和名称
// String fileName = "pdfNoMark_" + System.currentTimeMillis() + ".pdf";
// String pdfNoMarkPath = System.getProperty("java.io.tmpdir").replaceAll(separator + "$", "") + separator + fileName;
/**
* windows环境
*/
String pdfNoMarkPath = pdfPath;
/**
* linux环境
*/
// String pdfNoMarkPath = "/java/word/" + separator + fileName;
os = new FileOutputStream(pdfNoMarkPath);
//docx4j docx转pdf
FOSettings foSettings = Docx4J.createFOSettings();
foSettings.setWmlPackage(mlPackage);
Docx4J.toFO(foSettings, os, Docx4J.FLAG_EXPORT_PREFER_XSL);
is.close();//关闭输入流
os.close();//关闭输出流
} catch (Exception e) {
e.printStackTrace();
try {
if(is != null){
is.close();
}
if(os != null){
os.close();
}
}catch (Exception ex){
ex.printStackTrace();
}
}finally {
File file = new File(wordPath);
if(file!=null&&file.isFile()&&file.exists()){
file.delete();
}
}
}
}
测试word模板文件
导出效果
导出word
导出pdf
模板语法
可以参考easypoi的官方文档《模板 指令介绍》
自定义注解的使用
@ExportFiled
注解是使用在类属性上的,有以下几个属性
- name:属性的名字,会决定传入模板map中的key,默认是属性名;
- type:属性的类型,目前有三种类型,string、image和date,string就不介绍了(默认除了image和date就是string类型),image代表图片,如何属性的类型是image,那么此字段必须传图片的url。date代表日期,默认是使用的LocalDateTime,需要配合format使用;
- height:图片的高度,默认为200;
- width:图片的宽度,默认为500;
- format:日期的格式,默认格式为yyy-MM-dd HH:mm:ss;
- replace:值得替换,以"source_target,source_target"的形式替换。
注意事项
在通过easypoi导出word模板和通过docx4j将导出的word转换成pdf的过程中会出现很多问题,下面将介绍比较重要的几个注意事项。
- 遍历指令的使用
假设有如下的指令,其中一行遍历写在表格中,另一行遍历写在表格外
导出的结果如下,会发现只有表格内的指令生效了
因为easypoi本身是用来导出excel表格的,所以遍历的指令是没有兼容word模板的
- 表格中的遍历指令
假如有如下模板
导出的word结果如下
可以发现表格最后一列应有的数据不见了,而且可以推测数据是被覆盖而不是没有插入。
但是如果在遍历指令的下一行表格中插入数据
导出的表格中是存在插入的数据的
可以得出结论表格中如果出现遍历数据,那么同一行的其他数据会被覆盖(包括被空白覆盖)。而且还有如下的情况出现,如果遍历的指令没有写在第一列也是无法生效的。
- 图片的使用
如果想要在word中插入图片,需要将图片封装成一个ImageEntity
对象。需要注意的是如果使用的easypoi是4.2.0及以下的版本,那么easypoi只能接口单个图片对象或者一个list集合对象包含的一系列图片,不能将图片包含在map集合中。因为easypoi在处理map集合中只会单纯将其中的value转化成string对象,并没有考虑其他类型的对象。
假设有如下的测试对象,其中的list会被解析成list包含的map对象,其中就有图片对象
下面是导出的模板
导出的结果,可以发现图片并没有展现
通过debug可以看到图片对象确实已经注入到map中
这个问题已经在4.3.0版本中解决了,还是同样的模板和代码,导出的结果如下
- 图片错误的url
我们导出word的时候会遇到一个问题,那就是导出过程突然变慢,然后出现如下的错误,虽然最终还是可以将word导出,但是这样还是会影响使用。这个现象出现的原因就是图片的url地址有问题,然后easypoi去请求访问时连接时间会变长并超时,所以这里建议使用的图片地址一定要是正确并且能够快速访问的。
为了避免这种不友好的错误和过长时间的导出过程,我们可以借助下面这个工具类先将图片的url检测一遍再进行封装。
import java.net.URL;
import java.net.URLConnection;
/**
* @description:
* @author: zdd
* @time: 2021/5/24 11:28
*/
public class UrlUtil {
public static Boolean testUrlWithTimeOut(String urlString,int timeOutMillSeconds){
URL url;
try {
url = new URL(urlString);
URLConnection co = url.openConnection();
co.setConnectTimeout(timeOutMillSeconds);
co.connect();
return true;
} catch (Exception e1) {
return false;
}
}
}
- 图片的大小
仔细观察下面的图片,第一张图是word导出,第二张图是pdf导出。可以很轻易发现第一张图片的大小比例不相同,其实不光是第一张,后面两张图片的大小也有细微的不同。
通过docx4j转换而成的pdf中的图片必定会还原成原图的长宽比例,但是图片的大小比例还是会按照设置的长宽中最大的来生成。
- 模板的读取路径
还需要注意的是如果项目是打成jar包放在windows或者linux系统中执行的,那么模板的路径就和直接在idea中的不一致。可能存在以下的空指针异常,这是因为easypoi读取我们提供的路径发现并没有模板文件。
解决的办法有很多,这里提供的解决思路是通过文件流的方式读取模板文件,然后将文件copy到jar包外的指定路径中,最后使用copy出来的文件路径。
下面是转换路径的方法
public static String convertTemplatePath(String path){
FileOutputStream fileOutputStream = null;
// 将模版文件写入到tomcat临时目录
String folder = System.getProperty("catalina.home");
File tempFile = new File(folder + File.separator + path);
// 文件存在时不再写入
if (tempFile.exists()) {
return tempFile.getPath();
}
File parentFile = tempFile.getParentFile();
// 判断父文件夹是否存在
if (!parentFile.exists()) {
parentFile.mkdirs();
}
InputStream in = PdfUtil.class.getResourceAsStream(path);
try {
fileOutputStream = new FileOutputStream(tempFile);
byte[] buffer = new byte[10240];
int len = 0;
while ((len = in.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return tempFile.getPath();
}
其中参数是文件在jar包中的绝对路径,”/“
代表的就是下面所示的路径
下面这个模板文件的绝对路径就是"/templates/recDetailsTemp.docx"