根据一个word模板,在程序中替换模板中的参数,然后根据这个模板导出word文件。

引入POI对word操作的依赖

<dependency>
  <groupId>org.apache.poi</groupId>
  <artifactId>poi-ooxml</artifactId>
  <version>3.17</version>
</dependency>

引入的版本一定要高一点,低版本会出现导出的word中图片无法显示的BUG


创建WordUtils类(使用poi导出word模板的操作都封装在这了)

@Component
public class WordUtils {

	 /**
     * description:
     *          导出word模板
     * @param path:word模板路径
	 * @param params:模板中需要替换的参数可多个传递 比如 若想文字能够多行,在参数
	 *               Map<String,Object>中的Object访如List<string>
     *               若是传递图片 参数Map<String,Object>中的Object为Map<String,Obejct>
     * 				若是一个参数处要传入多张图片Object就为List<Map<String,Object>   
     *              在传入多张图片的时候 Map中设置style ,style=1 代表图片并排插入,
	 *		        style=2代表图片竖排插入,设置imgpath,即图片路径。
	 *				传递多张图片的时候imgpath和style两个参数必填,若是一张,imgpath必填	
     * @param filename:导出的word文件名
	 * @param response:
     * @return void
     */
    public void exportWord(String path, Map<String,Object> params, String filename, HttpServletResponse response) throws IOException, InvalidFormatException {
        InputStream is=new FileInputStream(path);
        XWPFDocument doc=new XWPFDocument(is);
        iteraTable(params,doc);
        OutputStream os=response.getOutputStream();
        //设置导出的内容是doc
        response.setContentType("application/octet-stream; charset=utf-8");
        response.setHeader("Content-disposition", "attachment; filename=" + filename);
        doc.write(os);
        close(os);
    }

    //开始遍历文档中的表格
    private  void iteraTable(Map<String,Object> params,XWPFDocument doc) throws IOException, InvalidFormatException {
        List<XWPFTableRow> rows=null;
        List<XWPFTableCell> cells=null;

        List<XWPFTable> tables=doc.getTables();
        //遍历这个word文档的所有table表格
        for (XWPFTable table :
                tables) {
            rows=table.getRows();
            //遍历这个表格的所有行
            for (XWPFTableRow row :
                    rows) {
                cells=row.getTableCells();
                //遍历这一行的单元格
                for (XWPFTableCell cell:
                        cells) {
                    //判断该单元格的内容是否是字符串字段
                    if(strMatcher(cell.getText()).find()){
                        //替换字符串 字符串可以多行 也可以一行
                        replaceInStr(cell,params,doc);
                        continue;
                    }
                    //判断该单元格内容是否是需要替换的图片
                    if(imgMatcher(cell.getText()).find()){
                        //把模板中的内容替换成图片 图片可以多涨
                        replaceInImg(cell,params,doc);
                        continue;
                    }
                }
            }
        }
    }

    //返回模板中图片字符串的匹配Matcher类
    private Matcher imgMatcher(String imgstr){
        Pattern pattern=Pattern.compile("@\\{(.+?)\\}");
        Matcher matcher=pattern.matcher(imgstr);
        return matcher;
    }

    //返回模板中变量的匹配Matcher类
    private  Matcher strMatcher(String str){
        Pattern pattern=Pattern.compile("\\$\\{(.+?)\\}");
        Matcher matcher=pattern.matcher(str);
        return matcher;
    }


    //替换模板Table中相应字段为对应的字符串值
    private  void replaceInStr(XWPFTableCell cell, Map<String,Object> params, XWPFDocument doc) {
        //两种数据类型  一种是直接String  还有一种是List<String>
        String key = cell.getText().substring(2, cell.getText().length() - 1);

        Integer datatype = getMapStrDataTypeValue(params, key);

        List<XWPFParagraph> parags = cell.getParagraphs();
        //先清空单元格中所有的段落
        for (int i = 0; i < parags.size(); i++) {
            cell.removeParagraph(i);
        }
        if (datatype.equals(0)) {
            return;
        } else if (datatype.equals(2)) {
            //如果类型是2 说明数据类型是List<String>
            List<String> strs = (List<String>) params.get(key);
            Iterator<String> iterator = strs.iterator();
            while (iterator.hasNext()) {
                XWPFParagraph para = cell.addParagraph();
                XWPFRun run = para.createRun();
                run.setText(iterator.next());
            }
        } else if (datatype.equals(3)) {
            String str = (String) params.get(key).toString();
            cell.setText(str);
        }
    }

    //替换模板Table中相应字段为图片
    private  void replaceInImg(XWPFTableCell cell,Map<String,Object> params,XWPFDocument doc) throws IOException, InvalidFormatException {
        //拿参数 判断一下是什么类型 怎么处理   一般来说Map中只有两种类型 一种是一个模板图片变量放一张图片 只有一个Map  一种是一个模板变量中放多涨图片List<Map> 还有一种是不存在的情况
        String key=cell.getText().substring(2,cell.getText().length()-1);
        Integer datatype=getMapImgDataType(params,key);
        List<XWPFParagraph> parags=cell.getParagraphs();
        //先清空单元格中所有的段落
        for (int i=0;i<parags.size();i++){
            cell.removeParagraph(i);
        }
        if(datatype.equals(0)){
            return;
        }else if(datatype.equals(1)){
            //处理单张图片
            XWPFParagraph parag=cell.addParagraph();
            Map<String,Object> pic=(Map<String,Object>)params.get(key);
            insertImg(pic,parag);
        }else if(datatype.equals(2)) {
            //处理多涨图片
            List<Map<String,Object>> pics=(List<Map<String,Object>>)params.get(key);
            Iterator<Map<String, Object>> iterator = pics.iterator();
            XWPFParagraph para = cell.addParagraph();
            Integer count = 0;
            while (iterator.hasNext()) {
                Map<String, Object> pic = iterator.next();
                //图片并排插入
                if (Integer.parseInt(pic.get("style").toString()) == 1) {
                    //不做处理 这段代码可注释
                }
                //图片竖排插入
                if (Integer.parseInt(pic.get("style").toString()) == 2) {
                    if (count > 0) {
                        para = cell.addParagraph();
                    }
                }
                insertImg(pic, para);
                count++;
            }
        }
    }


    //插入图片   run创建
    private  void insertImg(Map<String,Object> pic,XWPFParagraph para) throws IOException, InvalidFormatException {
        if(pic.get("imgpath").toString()==null)
            return;
        String picpath=pic.get("imgpath").toString();
        InputStream is=null;
        BufferedImage bi=null;
        if(picpath.startsWith("http")) {
            is=HttpUtils.getFileStream(picpath);
            bi=ImageIO.read(HttpUtils.getFileStream(picpath));
        }else {
            is = new FileInputStream(picpath);
            bi=ImageIO.read(new File(picpath));
        }
        XWPFRun run =para.createRun();
        //原图片的长宽
        Integer width=bi.getWidth();
        Integer heigh=bi.getHeight();
        Double much=80.0/width;
        //图片按宽80 比例缩放
        run.addPicture(is,getPictureType(picpath.substring(picpath.lastIndexOf(".")+1)),"", Units.toEMU(80),Units.toEMU(heigh*much));
        //图片原长宽
//        run.addPicture(is,getPictureType(pic.get("picType").toString()),"",Units.toEMU(width),Units.toEMU(heigh));
        close(is);
        bi=null;
    }

    //插入图片  自定义 doc方法
//    private static void insertImg(Map<String,Object> pic,XWPFParagraph para,CustomXWPFDocument doc) throws FileNotFoundException, InvalidFormatException {
//        InputStream is=null;
//        if(pic.get("imgpath").toString()==null)
//            return;
//        is=new FileInputStream(pic.get("imgpath").toString());
//        byte[] bytes=inputStream2ByteArray(is,true);
//        doc.addPictureData(bytes, getPictureType(pic.get("picType").toString()));
//        doc.createPicture(doc.getAllPictures().size() - 1, 30, 360,para);
//        close(is);
//    }


    //处理模板中的变量名字,去掉${}  然后根据这个变量名在参数map中查找对应的Value值
    private  Integer getMapStrDataTypeValue(Map<String,Object> params,String key){
        if(params.get(key)==null){
            return 0;
        }else if(params.get(key) instanceof List){
            return 2;
        }else if(params.get(key) instanceof String){
            return 3;
        }else {
            throw new RuntimeException("Str data type error!");
        }

    }

    //从params参数中找到模板中key对应的value值
    private  Integer getMapImgDataType(Map<String,Object> params,String key){
        if(params.get(key)==null){
            return 0;
        }
        else if(params.get(key) instanceof  Map){
            return 1;
        }else if(params.get(key) instanceof List){
            return 2;
        }else {
            throw  new RuntimeException("image data type error!");
        }
    }


    /**
     * 根据图片类型,取得对应的图片类型代码
     *
     * @param picType
     * @return int
     */
    private  int getPictureType(String picType) {
        int res = XWPFDocument.PICTURE_TYPE_PICT;
        if (picType != null) {
            if (picType.equalsIgnoreCase("png")) {
                res = XWPFDocument.PICTURE_TYPE_PNG;
            } else if (picType.equalsIgnoreCase("dib")) {
                res = XWPFDocument.PICTURE_TYPE_DIB;
            } else if (picType.equalsIgnoreCase("emf")) {
                res = XWPFDocument.PICTURE_TYPE_EMF;
            } else if (picType.equalsIgnoreCase("jpg") || picType.equalsIgnoreCase("jpeg")) {
                res = XWPFDocument.PICTURE_TYPE_JPEG;
            } else if (picType.equalsIgnoreCase("wmf")) {
                res = XWPFDocument.PICTURE_TYPE_WMF;
            }
        }
        return res;
    }

    /**
     * 将输入流中的数据写入字节数组
     *
     * @param in
     * @return
     */
    public  byte[] inputStream2ByteArray(InputStream in, boolean isClose) {
        byte[] byteArray = null;
        try {
            //获得输入流中还有多少字节可读取
            int total = in.available();
            byteArray = new byte[total];
            in.read(byteArray);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (isClose) {
                try {
                    in.close();
                } catch (Exception e2) {
                    e2.getStackTrace();
                }
            }
        }
        return byteArray;
    }


    /**
     * 关闭输入流
     *
     * @param is
     */
    private  void close(InputStream is) {
        if (is != null) {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 关闭输出流
     *
     * @param os
     */
    private  void close(OutputStream os) {
        if (os != null) {
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

由于图片有时候并不是本地资源,而是别的服务器上的静态资源,所以创建一个HttpUtils类,写一个方法,通过图片的URL来获得图片的输入流。

HttpUtils.java

public class HttpUtils {
    /**
     * description:
     *          通过图片URL地址得到文件流
     * @Author any
     * @Date 2020/8/14 11:51
     * @param url:图片地址
     * @return java.io.InputStream
     */
    public static InputStream getFileStream(String url){
        try {
            URL httpUrl = new URL(url);
            HttpURLConnection conn = (HttpURLConnection)httpUrl.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5 * 1000);
            InputStream inStream = conn.getInputStream();//通过输入流获取图片数据
            return inStream;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}

word模板

图片的前缀是@,如果是文字那么前缀是#,然后参数名用{}包裹起来。

模板示例

在这里插入图片描述

测试代码

@RestController
public class MainTest {
    @Autowired
    private WordUtils wordUtils;

    @RequestMapping("/test/mydocx")
    public void exportWord(HttpServletResponse response) throws IOException, InvalidFormatException {
		//模板路径
        String path="F:\\testmodel\\model.docx";
        //传入模板的参数
        Map<String,Object> params=new HashMap<>();
        //模板的导出文件名字
        String filename="mydocxtest.docx";

        //一个单元格内一行字
        params.put("name","张三");
        params.put("sex","男");

        //一个单元格内多行字
        List<String> hobby=new ArrayList<>();
        hobby.add("1、打篮球");
        hobby.add("2、打羽毛球");
        hobby.add("3、游泳");
        params.put("hobby",hobby);


        //图片参数1 多张图片 图片多行竖排排列 style为2
        List<Map<String,Object>> imgs1List=new ArrayList<>();
        Map<String,Object> img=new HashMap<>();
        img.put("style",2);
        img.put("imgpath","F:\\mytestimg\\testimg1.jpg");
        imgs1List.add(img);
        img=new HashMap<>();
        img.put("style",2);
        //放一张服务器上的图片资源
        img.put("imgpath","https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1597386736254&di=e25ddaabbe7e9259f00008f087cc8ab7&imgtype=0&src=http%3A%2F%2Fimg.mukewang.com%2Fszimg%2F5e3ae4bd096481b210320850.jpg");
        imgs1List.add(img);
        params.put("workimg",imgs1List);

        //图片参数2 多张图片 一行内横排排列 style为1
        List<Map<String,Object>> imgs2List=new ArrayList<>();
        Map<String,Object> img2=new HashMap<>();
        img2.put("style",1);
        img2.put("imgpath","F:\\mytestimg\\sign1.png");
        imgs2List.add(img2);
        img2=new HashMap<>();
        img2.put("style",1);
        img2.put("imgpath","F:\\mytestimg\\sign2.png");
        imgs2List.add(img2);
        img2=new HashMap<>();
        img2.put("style",1);
        img2.put("imgpath","F:\\mytestimg\\sign3.png");
        imgs2List.add(img2);
        params.put("signimg",imgs2List);

        //图片参数3 单张图片
        Map<String,Object> img3=new HashMap<>();
        img3.put("imgpath","F:\\mytestimg\\testimg1.jpg");
        params.put("otherimg",img3);
        wordUtils.exportWord(path,params,filename,response);

    }
}

模板中的参数 一定要和程序中传入的参数的key值一样,比如模板参数#{name},那么程序中的map参数的key为name,这样就能对应起来了。

效果

在这里插入图片描述

备注

  • 新建word后另存为Word 2003 XML文档(*.xml)类型即为模板文件,可以修改后缀为.ftl
  • 模板备注后变量千万不要直接填写为${变量}的形式,模板会将其分割,可以先在word中设定变量,生成模板后再将变量改为${变量}的形式
  • 项目部署需要提前将模板文件保存在服务器的项目路径的/templates下,有时间可修改此种方式不依赖于本地文件