java使用freemarker导出word(标准格式版)

时光毁灭记忆、已成空白 提交于 2019-11-29 10:38:18

之前使用freemarker导出word,可以在PC上打开。但是它不能在手机上打开,因为它是xml格式的。所以后来又修改了导出方式,导出标准格式的word.
(具体步骤和引用文件不再赘述,可先参考之前文章)
之前的导出方式:请戳====>java使用freemarker导出word

一、文件结构

首先我们来看一下标准格式的word有哪些东西组成。

  1. 和上文一样,创建一个word文档trip.docx。
  2. 把后缀名docx改为zip
    在这里插入图片描述在这里插入图片描述
    3. 解压后,查看文件夹内容结构如下:
    在这里插入图片描述
    在这里插入图片描述
  3. 查看各个文档后我们会发现
    根目录下的**[Content_Types].xml中指定文件的配置,比如图片格式,主题文件,样式文件等等;
    word文件夹下的
    media文件夹存放文档中的图片;
    word文件夹下的_rels文件夹下的
    document.xml.rels中则指定了文档中用的图片id和图片名称;
    word文件夹下的
    document.xml则是文档内容,也是我们要写入的文档模板。
    word文件夹下的
    header1.xml则是指定页眉
    这里没有写页脚,如果有页脚,会有
    footer.xml**的文件生成。
    想用和需要修改的就是上面几个文件了。

二、思路分析

  1. 使用document.xml修改成需要的word模板
  2. 修改header1.xml中的页眉
  3. 在**[Content_Types].xml**自动生成或提前写入图片格式
  4. media文件夹下输出图片
  5. document.xml.rels中动态生成图片,id与document.xml中保持一致,名字写入文件夹media的一致
  6. 指定一个模板文件,用于生成相同格式

三、模板修改

  1. 在resource文件夹下创建templates文件夹,把**[Content_Types].xml**、document.xmlheader1.xml****document.xml.rels放入templates中。
    在这里插入图片描述
  2. 其中**[Content_Types].xml**中的图片格式我没有动态生成,预先写入了我会用到的两种,如下:
<?xml version="1.0" encoding="utf-8"?>

<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
 
  <Default Extension="png" ContentType="image/png"/>
  <Default Extension="jpeg" ContentType="image/jpeg"/>
<!-- 其他配置省略-->
</Types>
  1. header1.xml写入页眉:${title}
<?xml version="1.0" encoding="utf-8"?>

<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 wp14">
  <w:p w:rsidR="006747D4" w:rsidRDefault="008E4089">
    <w:pPr>
      <w:pStyle w:val="a3"/>
    </w:pPr>
    <w:r>
      <w:t>${title}</w:t>
    </w:r>
  </w:p>
</w:hdr>
  1. document.xml.rels生成图片
<?xml version="1.0" encoding="utf-8"?>

<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
  <Relationship Id="rId12" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
  <Relationship Id="rId11" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
  <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" Target="endnotes.xml"/>
  <Relationship Id="rId10" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/>
  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="footnotes.xml"/>

  <!-- 注意id,需要和文件中对应,如果不对应,最终生成的文件会出错-->
  
  <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.jpeg"/>
  <Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image2.jpeg"/>
  <Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image3.jpeg"/>

<!-- 注意id的生成,避免重复。这里我自己在rId后又加了1作为前缀-->

   <#if dayDetail?? && (dayDetail?size > 0)>
	    <#list dayDetail as detail>
	        <#if detail.contents?? && (detail.contents?size > 0)>
	            <#list detail.contents as detailContent>
	                <#if (detailContent.url)?? && (detailContent.url) !="">
	                    <Relationship Id="rId1${detail_index+1}${detailContent_index+1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image${detail_index+1}${detailContent_index+1}.png"/>
	                </#if>
	            </#list>
	        </#if>
	    </#list>
	</#if>
</Relationships>

  1. document.xml生成需要的文档模板,一般类型就用${title}这种替代。这里主要列出图片格式。
<!-- 图片id-->
<wp:docPr id="${detail_index+1}${detailContent_index+1}" name="图片 ${detail_index+1}${detailContent_index+1}"/>

<!-- 图片名-->
 <pic:nvPicPr>
   <pic:cNvPr id="0" name="Picture ${detail_index+1}${detailContent_index+1}"/>
    <pic:cNvPicPr>
      <a:picLocks noChangeAspect="1" noChangeArrowheads="1"/>
    </pic:cNvPicPr>
  </pic:nvPicPr>

 <!-- 图片rId1-->
<a:blip r:embed="rId1${detail_index+1}${detailContent_index+1}" cstate="print">

代码编写

private Configuration configuration = null;
	
	@Value("${filePath}")
	private String filePath;
	
	@Value("${filePathAlias}")
	private String filePathAlias;
	
	private final static String separator = File.separator;
    private final static String suffix_docx = "docx";
    
    private final static String docxTemplateFile = "docTemplates.docx";
    private final static String xmlDocument = "document.xml";
    private final static String xmlDocumentXmlRels = "document.xml.rels";
    private final static String xmlContentTypes = "[Content_Types].xml";
    private final static String xmlHeader = "header1.xml";//可以用来修改页眉的一些信息
	
	@PostConstruct
	public void init(){
		configuration = new Configuration(Configuration.VERSION_2_3_28);
		configuration.setDefaultEncoding("UTF-8");
		configuration.setClassForTemplateLoading(this.getClass(), "/templates/");
	}

	@Override
	public String exportWord(TripExportDTO u) throws Exception {
		String url = this.createDocx(u);
		return url;
	}
@SuppressWarnings("resource")
	private String createDocx(TripExportDTO u) throws Exception {
		String timeStr = DateUtils.date2Str(new Date(), DateUtils.DEFAULT_DATETIME_PATTERN_TRIM);
        String templatePath = separator + "templates" + separator;
        String urlFileName = URLEncoder.encode(u.getTitle(), "UTF-8") + "-" + timeStr + "."+ suffix_docx;
        String outputFileName = u.getTitle() + "-" + timeStr + "."+ suffix_docx;
        
        //这里生成的文件路径和返回出去的文件路径不同,做了文件路径跳转
		String outputFileTempPath = "temp" + separator + timeStr + "-" + u.getId() + separator;
		String realTemplatePath = filePath + templatePath;
		String outputPath = filePath + outputFileTempPath;
		
		//获取 document.xml.rels 输入流
        ByteArrayInputStream documentXmlRelsInput = this.getFreemarkerContentInputStream(u, xmlDocumentXmlRels);
        //获取 header1.xml 输入流
        ByteArrayInputStream headerInput = this.getFreemarkerContentInputStream(u, xmlHeader);
        //获取 [Content_Types].xml 输入流
        ByteArrayInputStream contentTypesInput = this.getFreemarkerContentInputStream(u, xmlContentTypes);
        //获取 document.xml 输入流
        ByteArrayInputStream documentInput = this.getFreemarkerContentInputStream(u, xmlDocument);

        
        File docxFile = new File(realTemplatePath + separator + docxTemplateFile);
        if (!docxFile.exists()) {
            docxFile.createNewFile();
        }

        ZipFile zipFile = new ZipFile(docxFile);
        Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();
        File tempPath = new File(outputPath);
        //如果输出目标文件夹不存在,则创建
        if (!tempPath.exists()) {
            tempPath.mkdirs();
        }
        ZipOutputStream zipout = new ZipOutputStream(new FileOutputStream(outputPath + outputFileName));

        //------------------覆盖文档------------------
        int len = -1;
        byte[] buffer = new byte[1024];
        while (zipEntrys.hasMoreElements()) {
            ZipEntry next = zipEntrys.nextElement();
            InputStream is = zipFile.getInputStream(next);
            if (next.toString().indexOf("media") < 0) {
                // 把输入流的文件传到输出流中 如果是word/document.xml由我们输入
                zipout.putNextEntry(new ZipEntry(next.getName()));
                //写入图片配置类型
                if (next.getName().equals("[Content_Types].xml")) {
                    this.writeOutputStream(contentTypesInput, zipout, buffer, len);
                } else if (next.getName().indexOf("document.xml.rels") > 0) {
                    //写入填充数据后的主数据配置信息
                    this.writeOutputStream(documentXmlRelsInput, zipout, buffer, len);
                } else if ("word/document.xml".equals(next.getName())) {
                    //写入填充数据后的主数据信息
                    this.writeOutputStream(documentInput, zipout, buffer, len);
                } else if ("word/header1.xml".equals(next.getName())) {
                    //写入填充数据后的页眉信息
                    this.writeOutputStream(headerInput, zipout, buffer, len);
                } else {
                    while ((len = is.read(buffer)) != -1) {
                        zipout.write(buffer, 0, len);
                    }
                    is.close();
                }
            }
        }

        //------------------写入新图片------------------
        len = -1;
		if(StringUtils.isNotBlank(u.getCoverImg())) {
			this.writeImage("image1.jpeg", u.getCoverImg(), zipout, buffer);
		}
		
		if(StringUtils.isNotBlank(u.getQrCode())) {
			this.writeImage("image2.jpeg", u.getQrCode(), zipout, buffer);
		}
		
		if(StringUtils.isNotBlank(u.getGoogleMap())) {
            this.writeImage("image3.jpeg", u.getGoogleMap(), zipout, buffer);
		}
		
		if(CollectionUtils.isNotEmpty(u.getDayDetail())) {
			for(int i = 1;i<= u.getDayDetail().size() ; i++) {
				TripDetailDTO tripDetail = u.getDayDetail().get(i-1);
				if(CollectionUtils.isNotEmpty(tripDetail.getContents())) {
					for(int j = 1; j<= tripDetail.getContents().size(); j++) {
						TripContentDTO tripContent = tripDetail.getContents().get(j-1);
						if(StringUtils.isNotBlank(tripContent.getUrl())) {
							writeImage("image" + i + j + ".png", tripContent.getUrl() , zipout, buffer);
						}
					}
				}
			}
		}
		
	    zipout.close();
	    
		return filePathAlias + outputFileTempPath + urlFileName;
	}
private void writeImage(String imageName, String path, ZipOutputStream zipout, byte[] buffer) throws Exception {
		int len = -1;
		ZipEntry next = new ZipEntry("word" + separator + "media" + separator + imageName);
        // 创建URL
        URL url = new URL(path);
        // 创建链接
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setConnectTimeout(5000);
        InputStream in = conn.getInputStream();
		 
        zipout.putNextEntry(new ZipEntry(next.toString()));
        while ((len = in.read(buffer)) != -1) {
            zipout.write(buffer, 0, len);
        }
        
        in.close();
	}
	
	public ByteArrayInputStream getFreemarkerContentInputStream(Object dataMap, String templateName) throws Exception {
        //获取模板
        Template template = configuration.getTemplate(templateName);
        StringWriter swriter = new StringWriter();
        //生成文件
        template.process(dataMap, swriter);
        ByteArrayInputStream in = new ByteArrayInputStream(swriter.toString().getBytes());
        return in;
	}
	
	private void writeOutputStream(ByteArrayInputStream input, ZipOutputStream zipout , byte[] buffer, int len) throws Exception {
		if (input != null) {
            while ((len = input.read(buffer)) != -1) {
                zipout.write(buffer, 0, len);
            }
            input.close();
        }
	}

注意特殊字符

  • 由于freemarker在<、>、&这三个字符识别时会导致文档打不开,所以需要对文本中的这些字符需要转换
private String transform(String str){
		if(StringUtils.isBlank(str)) {
			return str;
		}
		
		if(str.contains("<")||str.contains(">")||str.contains("&")){
			  str=str.replaceAll("&", "&amp;");
			  str=str.replaceAll("<", "&lt;");
			  str=str.replaceAll(">", "&gt;");
		}
	
		return str;
	}
  • 文件名如果有空格,URLEncoder.encode后空格会被转为+,可以使用
    fileName = fileName.replaceAll("\+","%20");

以上再生成的word文档就是标准格式的文档了。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!