公司前一段时间做的一个项目中应用到了这个技术,需要后台获取数据后在前台直接下载word文档,利用freemarker模版生成的doc文档在电脑上可以正常打开,但是发送到手机上打开则全部变成“乱码”。但实际上并不是乱码,而是xml格式的代码,在手机等移动端显示不出正常的文档信息。之后辗转查询使用了很多方案,比如利用poi操作,利用jacob进行格式转换,但是效果都不尽如人意,而且处理过程非常复杂,浪费了大量时间。最终,通过不断摸索,找到了非常完美的解决方案。不需要先生成xml格式的doc文档再去转换,而是戒指在后台利用模版添加数据后,利用IO流进行替换,可以直接生成标准的docx文档。

    首先需要理解的一点是,docx文档本身是一个压缩文件。在利用这个方案前要先准备好docx模版。在这个docx模版中,将要替换的数据用${}的方式替换掉,先占好位置。如图:

java 向doc文档 java生成docx文件_xml

    将docx改后缀名为.zip并解压出来。在word文件夹下有一个文件为、:document.xml

    利用notpad等工具打开此文件,将格式修改好,比如在xml文件中${xxx}分别氛围${ ,  xxx 以及 } 三部分存贮在不同的标签下,要将他们合并到一起。

    随后将保存好的document.xml和之前定义好的zip文件放入工程的配置文件夹下,方便引用。

    核心代码如下

Configuration
             configuration = new Configuration();
     /** 设置编码 **/
     /** 我的ftl文件是放在D盘的**/
     String fileDirectory = "D:/cache/qqChace/T1/xlsx";
     /** 加载文件 **/
     configuration.setDirectoryForTemplateLoading(new File(fileDirectory));
     /** 加载模板 **/
     Template template = configuration.getTemplate("document.xml");
     /** 准备数据 **/
     Map<String,String> dataMap = new HashMap<>();
     /** 在ftl文件中有${textDeal}这个标签**/

     dataMap.put("id","哈士奇");
     dataMap.put("number","20");
     dataMap.put("language","java,php,python,c++.......");
     dataMap.put("example","Hello World!");

     /** 指定输出word文件的路径 **/
     String outFilePath = "D:/cache/qqChace/T1/xlsx/data.xml";
     File docFile = new File(outFilePath);
     FileOutputStream fos = new FileOutputStream(docFile);
     OutputStreamWriter oWriter = new OutputStreamWriter(fos);
     Writer out = new BufferedWriter(new OutputStreamWriter(fos),10240);
     template.process(dataMap,out);

     if(out != null){
         out.close();
     }
     // ZipUtils 是一个工具类,主要用来替换具体可以看github工程
     ZipInputStream zipInputStream = ZipUtils.wrapZipInputStream(new FileInputStream(new File("D:/cache/qqChace/T1/xlsx/test.zip")));
        ZipOutputStream zipOutputStream = ZipUtils.wrapZipOutputStream(new FileOutputStream(new File("D:/cache/qqChace/T1/xlsx/test.docx")));
        String itemname = "word/document.xml";
        ZipUtils.replaceItem(zipInputStream, zipOutputStream, itemname, new FileInputStream(new File("D:/cache/qqChace/T1/xlsx/data.xml")));
        System.out.println("success");

   上面主要解决的问题事文字的转换,下面要说的关于图片的转换问题。

    将zip文件解压后会发现,document.xml中没有你原本保存在docx中的图片,图片是在word下的media文件夹下。document.xml中或许只是引用。但是仍可以通过这种方式,以流的形式替换图片。只要保证图片的顺序不乱就可以了。

   下面是我在上面这段代码上进行的改动

String zipPath =  this.getClass().getResource("/freemarkerDoc.zip").getPath();
	        try {
	            ZipInputStream zipInputStream = ZipUtils.wrapZipInputStream(new FileInputStream(new File(zipPath)));
	            ZipOutputStream zipOutputStream = ZipUtils.wrapZipOutputStream(new FileOutputStream(new File(basePath+patientInfo.getName()+"连续血氧检测报告("+reportInfo.getId()+").docx")));//这个是要生成文档的路径和名称
	            String itemname = "word/document.xml";
	            String itemname2 = "word/media/";
	            String picturePath = PropertiesUtil.getBinaryRoorPath("picturePath")+File.separator+patientInfo.getId()+File.separator+reportInfo.getId()+File.separator;
	            ZipUtils.replaceItem(zipInputStream, zipOutputStream, itemname, new FileInputStream(new File(url)), itemname2, picturePath);
	            System.out.println("success");

	        } catch (Exception e) {
	            System.out.println(e.toString());
	        }

我的docx文档中工有四张图片,所以在zipUtil中要传入你要替换上去的图片路径。代码如下

public static void replaceItem(ZipInputStream zipInputStream,
                                   ZipOutputStream zipOutputStream,
                                   String itemName,
                                   InputStream itemInputStream
                                   ,
                                   String itemName2,
                                   String picturePath
    ){
        //
        if(null == zipInputStream){return;}
        if(null == zipOutputStream){return;}
        if(null == itemName){return;}
        if(null == itemInputStream){return;}
        InputStream itemIS = null;
        //
        ZipEntry entryIn;
        try {
            while((entryIn = zipInputStream.getNextEntry())!=null)
            {
                String entryName =  entryIn.getName();
                ZipEntry entryOut = new ZipEntry(entryName);
                // 只使用 name
                zipOutputStream.putNextEntry(entryOut);
                // 缓冲区
                byte [] buf = new byte[8*1024];
                int len;
                //image1.png等是media目录下的图片名称。
                String ima1 = itemName2+"image1.png";
                String ima2 = itemName2+"image2.png";
                String ima3 = itemName2+"image3.png";
                String ima4 = itemName2+"image4.png";
                
                if(entryName.equals(itemName)){
                    // 使用替换流
                    while((len = (itemInputStream.read(buf))) > 0) {
                        zipOutputStream.write(buf, 0, len);
                    }
                }else if(entryName.equals(ima1)){
                    // 使用替换流,将需要替换上去的图片路径以流的形式获取
                	itemIS = new FileInputStream(new File(picturePath+"spo2PiePicture.png"));
                    while((len = (itemIS.read(buf))) > 0) {
                        zipOutputStream.write(buf, 0, len);
                    }
                }else if(entryName.equals(ima2)){
                  // 使用替换流
            		itemIS = new FileInputStream(new File(picturePath+"prPiePicture.png"));
	                while((len = (itemIS.read(buf))) > 0) {
	                    zipOutputStream.write(buf, 0, len);
	                }
                }else if(entryName.equals(ima3)){
                // 使用替换流
                	itemIS = new FileInputStream(new File(picturePath+"spo2Picture.png"));
	                while((len = (itemIS.read(buf))) > 0) {
	                   zipOutputStream.write(buf, 0, len);
	                }
                }else if(entryName.equals(ima4)){
                    // 使用替换流
                	itemIS = new FileInputStream(new File(picturePath+"prPicture.png"));
	                while((len = (itemIS.read(buf))) > 0) {
	                    zipOutputStream.write(buf, 0, len);
	                }
                }else {
                    // 输出普通Zip流
                    while((len = (zipInputStream.read(buf))) > 0) {
                        zipOutputStream.write(buf, 0, len);
                    }
                }
                // 关闭此 entry
                zipOutputStream.closeEntry();

            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //e.printStackTrace();
            close(itemInputStream);
            close(zipInputStream);
            close(zipOutputStream);
            close(itemIS);
        }
    }

转换完成后要讲所有的流全部关闭,就完成了。