前言:
1. 因为项目需求,需要将业务数据导出word。所以想到了用word模板,然后用数据渲染。
2. 既然要做word模板引擎,自然需要选择一种引擎。刚开始我选用的是Poi-tl,但是很可惜,项目用的poi版本过低,要用的话,需要改很多地方。所以舍弃了,不过感觉Poi-tl比较方便。因为模板是docx文档,不存在我下面用freeMarker的问题。
3. Poi-tl 链接:https://github.com/Sayi/poi-tl 。很不错的word模板引擎,感兴趣的可以去试试。也许以后会写关于poi-tl的文章。
准备工作:
1. 第一步先下载依赖,我用的是maven,所以只需要再pom文件添加依赖即可。
2. 第二步创建特殊字符过滤类(此类的作用是解决生成的word含有特殊字符,从而打不开。因为项目是用户输入的数据,中间存在html转义,一旦转义后,将生成的数据渲染到word文档中就会打不开。注:只要保证生成word没有转义字符,此类可以不要)
/**
* 导出word的时候过滤并替换特殊字符
*
* @author hhs
* @version 2018-12-05 17:16
*/
public class WordReplaceUtils {
/**
* 将map中的所有value值含有特殊字符过滤掉
* @param map 渲染到模板的数据
* @throws IllegalAccessException
*/
public static void Wordreplace(HashMap<Object,Object> map) throws IllegalAccessException {
//先获取所有的key值
for (Object o : map.keySet()) {
//根据key值获取value值
Object value = map.get(o);
//如果value的属性是 String类型,就直接将特殊字符替换掉
if(value instanceof String){
//先将中文引号替换成英文引号
String replace = ((String) value).replace("“","\"").replace("”","\"");//替换中文引号符号
//再替换其他特殊字符
replace = replace.replaceAll("&[a-zA-Z]+;", " ");//替换所有&开头;结尾的字符
//重新设置value值,此处value值含有特殊字符的都已经过滤掉了。
map.put(o,replace);
}else if(value instanceof User ){
//因为用到了自定义类,所以value值可能是自定义类
Class<?> aClass = value.getClass();//先获取自定义类的class文件
Field[] declaredFields = aClass.getDeclaredFields();//再根据class获取所有属性字段
//遍历所有属性字段
for (Field declaredField: declaredFields) {
//设置可以访问,如果是私有的属性字段,必须加这一段
declaredField.setAccessible(true);
//根据自定义对象获取该属性字段的值
Object o1 = declaredField.get(value);
//如果属性字段值为String,就替换后,再赋值过去
if (o1 instanceof String) {
//此处和上面一样
String replace = ((String) o1).replace("“", "\"").replace("”", "\"");//替换中文引号符号
replace = replace.replaceAll("&[a-zA-Z]+;", " ");//替换所有&开头;结尾的字符
declaredField.set(value, replace);//重新将属性值赋给该自定义对象,map存储的value就会变了。如果不理解,可以看HashMap存储原理
}
}
}
}
}
//测试正则是否对,用于匹配特殊字符,然后替换掉
//特殊字符为&开头;结尾
public static void main(String[] args){
String a ="&mmmm;211221&rmod;&zmmm;232322&rmk;2232adad";
String s = a.replaceAll("&[a-zA-Z]+;", " ");
System.out.println(s);
}
}
3. 创建通用生成word类(重要)
/**
* @author hhs
* @version 2018-12-03 13:14
*/
public class WordGenerator {
private static Configuration configuration = null;
private static String url ="D:\\template";//模板我们放在D盘下面的template文件夹下
//进行freeMarker渲染前的设置,只用设置一次,所以写到了静态代码块中。
static {
configuration = new Configuration();
try {
//设置模板加载基础路径,请根据实际需求设置模板加载基础路径
configuration.setDirectoryForTemplateLoading(new File(url));
} catch (IOException e) {
e.printStackTrace();
}
//设置渲染字符集
configuration.setDefaultEncoding("UTF-8");
//设置模板异常处理
configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
}
//可以忽略,没有什么用,只是防止被实例化
private WordGenerator() {
throw new AssertionError();
}
/**
* 渲染doc核心方法
* @param dataMap 渲染数据
* @param xmlName 要用的模板,要保证模板加载基础路径+xmlName的路径能够找到该模板
* @return 渲染后的文件
*/
public static File createDoc(Map<?, ?> dataMap, String xmlName) {
//渲染后的临时文件名,因为我们要提供浏览器下载,所以该文件当浏览器下载完就会删掉
String name = "temp" + (int) (Math.random() * 100000) + ".doc";//保证不重名
File f = new File(name);//得到临时文件
Template t = null;//定义模板
try {
//替换特殊字符,防止生成的word打不开,如果不打算写这个方法,可以去掉
WordReplaceUtils.Wordreplace((HashMap<Object, Object>) dataMap);
t = configuration.getTemplate(xmlName);//得到模板
// 这个地方不能使用FileWriter因为需要指定编码类型否则生成的Word文档会因为有无法识别的编码而无法打开
Writer w = new OutputStreamWriter(new FileOutputStream(f), "utf-8");
t.process(dataMap, w);//渲染模板
w.close();//关闭流
} catch (Exception ex) {
ex.printStackTrace();
throw new RuntimeException(ex);
}
return f;
}
}
4. 准备模板 (请根据自己的实际情况创建模板)
- 先创建一个word文档另保存成xml,内容如图。
- 再用文本方式打开,ctrl+F搜索$,如图所示:
- 再删除无用的标签,是${变量}连起来,并替换成${user.loginName!""}类似的结构(因为如果中间有标签,freeMarker当成变量解析。!"" 代表如果变量为null,就输出空的字符串,如果不加这个,渲染的时候会报错。!"" 一定要是英文),修改之后的结果如图:
开始
1. 准备工作完成,接下来的是一个例子,请根据自己的实际情况使用。
/**
* 生成word
*
* @author hhs
* @version 2018-12-07 12:40
*/
public class CreateDoc {
@Test
public void test(){
HashMap<Object, Object> map = new HashMap<>();
User user = getUser();//得到数据,一般这步是从数据库获取的
setMap(map,user);//将数据放到map中
String filepath = "userTemplate.xml";//模板文件,位于D://template目录下。请看WordGenerator类的设置。
File doc = WordGenerator.createDoc(map, filepath);//渲染文件,并生成文件
//下载文件,省略。
System.out.println(doc.getAbsolutePath());//打印文件的绝对路径
}
/**
* 获取数据
* @return
*/
private User getUser() {
User user = new User();
user.setLoginName("张三");
user.setEmail("xxxxxx@qq.com");
user.setRemarks("这个是“”中文符号");
return user;
}
/**
* 将数据填充到map中
* 此处可以对数据进行处理(业务方面)
* @param map
* @param user
*/
private void setMap(HashMap<Object, Object> map, User user) {
map.put("user",user);
}
}
2. 查看生成的文件(此处为了证明生成文件是否正确)
问题总结
- 模板一定要把${}合并在一起,不然freeMarker解析不了,合并的时候,可以去ctrl+H删除无用的标签。
- WordReplaceUtils类一定要根据需求过滤不需要的转义字符,当文档打不开的时候,会提醒你第几行第几列实体出错,可以先以文本形式打开,看看有什么不同。然后过滤或转义相关字符。
- User是一个自定义类。
- 每通过word调整模板的时候,保存后一定要通过第一步保证${}是合并在一起的。(PS:我改了好几次模板,看的眼都花了。)
- 暂时能想到的问题就这么多。