一、简要描述
由于 easypoi对 word导出支持有限,也不能完成多记录导出。因此本次选择使用 freemarker来实现按自定义模板导出word。
使用 freemarker生成 word文档,基本流程如使用 easypoi一样简单,唯一的难点在于掌握如何配置 word模板,修改模板内容!
第一步:创建Word模板
(1)建议使用 office创建 word文档。原因:创建完成后,可轻松转为 Word 2003 XML文档(*.xml)
格式。
(2)xml 内容修改(重点)
1.模板制作成功后,可使用 Visual Studio Code 或者 IDEA
等工具打开 xml文件,打开后如下图:
红色部分:由于 xml文档内容非常多,为了便于修改,可在 Visual Studio Code 商店
安装 xml格式化插件。
绿色部分:xml 文档内容虽多,但很多内容是 word相关信息自带,无需关注。
2、直接往下翻,找到 <w:body> </w:body> 标签
,有点类似于 HTML,这里面存放的是模板内容,重要标签解释如下图:
注:上图中,不是"图片转成 base64",而是"图片内容的16进制字符串,失误,懒得改图了,特此注明!"
关于图片内容填充,此处我举个多图片例子,代码如下:
<w:p wsp:rsidR="00FE3676" wsp:rsidRDefault="009F529F">
<#list image as img>
<w:r>
<w:pict>
<w:binData w:name="${"wordml://img_"+img_index+".jpg"}" xml:space="preserve">${
img.picture}</w:binData>
<v:shape id="_x0000_i1026" o:spid="_x0000_i1025" type="#_x0000_t75"
style="width:384pt;height:216pt;visibility:visible;mso-wrap-style:square">
<v:imagedata src="${"wordml://img_"+img_index+".jpg"}" o:title="2"/>
</v:shape>
</w:pict>
</w:r>
</#list>
</w:p>
(在博客底部,我会附上一份完整的 xml文件。该文件内容为 `多条记录循环,每条记录里会有多张图片的模板`)
(3)将 xml
文件后缀改为 ftl
文件,至此模板制作成功。
第二步:导入 Jar包
<!-- freemarker -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
第三步:Controller 层接口
@ApiOperation(value = "导出 word文档")
@PostMapping("/word/export")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "用户id")})
public void wordExport(@RequestBody Long id,HttpServletRequest request, HttpServletResponse response) throws Exception {
service.wordExport(id, request, response);
}
第四步:Service 层业务
@Override
public void wordExport(Long id, HttpServletRequest request, HttpServletResponse response) throws IcmsException, IOException {
// 1.查询数据
User user = userDao.selectList(new QueryWrapper<User>().eq("id",id));
// 2.创建 map
Map<String,Object> data = new HashMap<>();
data.put("name",StringUtils.isEmpty(user.getName()) ? " ":user.getName()); // 姓名
data.put("age", null == user.getAge() ? " ":user.getAge()); // 年龄
data.put("sex",StringUtils.isEmpty(user.getSex()) ? " ":user.getSex()); // 性别
StringBuilder builder = new StringBuilder();
builder.append("<w:p></w:p>水果名称:草莓");
builder.append("<w:p></w:p>产地:海南");
data.put("content",builder.toString()); // 动态内容
// 3.处理图片
List<Map<String, Object>> resultPic = new ArrayList<>();
List<FileInfoModel> files = user.getFiles();
if(null != files && files.size() > 0){
for (int i = 0; i < files.size(); i++) {
// 判断图片路径是否为空
if(StringUtils.isEmpty(files.get(i).getUrl()))
continue;
Map<String, Object> pic = new HashMap<>();
String httpPath = request.getScheme() + "://" + request.getServerName() + files.get(i).getUrl(); // 图片路径
pic.put("picture", StringUtils.isEmpty(FileUtils.getImageString(httpPath)) ? " ":FileUtils.getImageString(httpPath));
resultPic.add(pic);
}
}else{
Map<String, Object> pic = new HashMap<>();
pic.put("picture", " ");
resultPic.add(pic);
}
map.put("image",resultPic);
// 4.执行导出
FileUtils.freemarkerExport("C:/Users/Administrator/Desktop","test.ftl",data,"C:/Users/Administrator/Desktop","test.docx",response);
}
关于业务描述,需要注意的点,和使用 easypoi一致。
我的另一篇博客:easypoi word 导出教程
第五步:FileUtils
通用工具类
public class FileUtils {
/**
* 使用 freemarker 生成word文档
*
* @param templateDir 模板所在目录路径
* @param templateName 模板 例如:xxx.ftl
* @param data 数据
* @param fileSavePath 文档生成后,存放的路径
* @param filename 生成后的文件名称,使用英文
* @param response
* @throws Exception
*/
public static void freemarkerExport(String templateDir, String templateName, Map<String, Object> data, String fileSavePath, String filename, HttpServletResponse response) throws Exception {
// 1.设置 freeMarker的版本和编码格式
Configuration configuration = new Configuration();
configuration.setDefaultEncoding("UTF-8");
// 2.设置 freeMarker生成Word文档,所需要的模板的路径
configuration.setDirectoryForTemplateLoading(new File(templateDir));
// 3.设置 freeMarker生成Word文档所需要的模板 ---> xxx.ftl
Template t = null;
try {
t = configuration.getTemplate(templateName); // 模板文件名称
} catch (IOException e) {
throw new IOException("获取 ftl模板失败!" + e.getMessage());
}
// 4.生成 Word文档的全路径名称
File outFile = new File(fileSavePath + filename);
// 5.创建一个 Word文档的输出流
Writer writer = null;
try {
writer = new OutputStreamWriter(new FileOutputStream(outFile), "utf-8");
} catch (Exception e) {
throw new Exception(e.getMessage());
}
try {
// 6.装载数据
t.process(data, writer);
response.setCharacterEncoding("utf-8");
response.addHeader("Content-Disposition", "attachment;filename=" + filename);
response.setContentType("application/force-download");
// 7.读取生成好的 Word文档
File file = new File(fileSavePath + filename);
FileInputStream is = new FileInputStream(file);
OutputStream os = response.getOutputStream();
byte[] b = new byte[1024];
int length;
while ((length = is.read(b)) > 0) {
os.write(b, 0, length);
}
os.flush();
os.close();
writer.flush();
writer.close();
} catch (IOException e) {
throw new IOException(e.getMessage());
} finally {
deleteTempFile(fileSavePath + filename);
}
}
/**
* 删除临时生成的文件
*/
public static void deleteTempFile(String filePath) {
File f = new File(filePath);
f.delete();
}
}
至此,按照以上顺序,即可完成 freemarker 导出 word!
二、扩展
1、图片转16进制字符串,并使用 base64编码。本地开发环境
public static String getImageString(String path) throws Exception {
InputStream input = new FileInputStream(path);
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int numBytesRead = 0;
while ((numBytesRead = input.read(buf)) != -1) {
output.write(buf, 0, numBytesRead);
}
byte[] data = output.toByteArray();
output.close();
input.close();
BASE64Encoder ecoder = new BASE64Encoder();
return ecoder.encode(data);
}
2、图片转16进制字符串,并使用 base64编码。Linux 生产环境
/**
* 将图片转换为 String存储
* @param path 文件 http路径
* @return String
* @throws IOException
*/
public static String getImageString(String path) throws IOException {
byte[] data = null;
URL url = null;
InputStream input = null;
try{
url = new URL(path);
HttpURLConnection httpUrl = (HttpURLConnection) url.openConnection();
httpUrl.connect();
httpUrl.getInputStream();
input = httpUrl.getInputStream();
}catch (Exception e) {
e.printStackTrace();
return null;
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int numBytesRead = 0;
while ((numBytesRead = input.read(buf)) != -1) {
output.write(buf, 0, numBytesRead);
}
data = output.toByteArray();
output.close();
input.close();
BASE64Encoder ecoder = new BASE64Encoder();
return ecoder.encode(data);
}
三、xml模板
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?mso-application progid="Word.Document"?>
<w:wordDocument xmlns:aml="http://schemas.microsoft.com/aml/2001/core" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml" xmlns:wx="http://schemas.microsoft.com/office/word/2003/auxHint" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wsp="http://schemas.microsoft.com/office/word/2003/wordml/sp2" xmlns:sl="http://schemas.microsoft.com/schemaLibrary/2003/core" w:macrosPresent="no" w:embeddedObjPresent="no" w:ocxPresent="no" xml:space="preserve">
<w:ignoreSubtree w:val="http://schemas.microsoft.com/office/word/2003/wordml/sp2"/>
<o:DocumentProperties>
<o:Author>Windows 用户</o:Author>
<o:LastAuthor>Windows 用户</o:LastAuthor>
<o:Revision>1</o:Revision>
<o:TotalTime>1</o:TotalTime>
<o:Created>2021-03-25T11:03:00Z</o:Created>
<o:LastSaved>2021-03-25T11:04:00Z</o:LastSaved>
<o:Pages>1</o:Pages>
<o:Words>0</o:Words>
<o:Characters>1</o:Characters>
<o:Company>Microsoft</o:Company>
<o:Lines>1</o:Lines>
<o:Paragraphs>1</o:Paragraphs>
<o:CharactersWithSpaces>1</o:CharactersWithSpaces>
<o:Version>16</o:Version>
</o:DocumentProperties>
<w:fonts>
<w:defaultFonts w:ascii="等线" w:fareast="等线" w:h-ansi="等线" w:cs="Times New Roman"/>
<w:font w:name="Times New Roman">
<w:panose-1 w:val="02020603050405020304"/>
<w:charset w:val="00"/>
<w:family w:val="Roman"/>
<w:pitch w:val=