前言
FreeMarker是一款模板引擎,即一种基于模板和要改变的数据,并用来生成输出文本的通用工具。


官方文档:http://freemarker.foofun.cn
日常开发中,我们会有这样的需求:
1、在页面上填写数据并导出word文档、后台批量设置数据并导出Excel文档(例如我们之前的博客记录:html页面转PDF、图片操作记录)
2、写一个代码生成工具(例如我们之前的博客记录:写一个简单的代码生成器)
这种情况下我们可以用FreeMarker模板引擎来实现,本文记录FreeMarker简单操作。
代码编写
maven引入依赖
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.31</version>
</dependency>
Word/Excel操作
Word操作
例如,要导出员工档案,格式如下:

我们先按照格式要求,写好word文档,然后另存为xml,得到模板文件

用idea打开word.xml,格式化一下,然后根据FreeMarker的语法表达式读取、设置值


封装一个方法
//Word文档操作
private static void createWord(){
System.out.println("开始Word文档操作...");
//指定TemplateLoading模板根路径
String rootPath = "E:\\Java\\test\\";
//模板文件
String templatePath = "word.xml";
//最终输出文件路径
String outFilePath = rootPath+"word_by_freemarker.docx";
//数据
Map<String, Object> data = new HashMap<>();
data.put("company","某某公司");
data.put("number","0001");
data.put("name","huanzi-qch");
data.put("phone","15600000000");
data.put("department","软件开发部");
data.put("post","开发工程师");
ArrayList<Map<String, String>> works = new ArrayList<>();
Map<String, String> work1 = new HashMap<>();
work1.put("company","某某单位1");
work1.put("time","2018-01 - 2019-01");
work1.put("department","研发1部");
work1.put("post","开发工程师");
works.add(work1);
Map<String, String> work2 = new HashMap<>();
work2.put("company","某某单位2");
work2.put("time","2019-01 - 2020-01");
work2.put("department","研发2部");
work2.put("post","开发工程师");
works.add(work2);
data.put("works",works);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){
//初始化,指定版本与pom文件相同
Configuration configuration = new Configuration(new Version("2.3.31"));
configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString());
configuration.setDirectoryForTemplateLoading(new File(rootPath));
Template template = configuration.getTemplate(templatePath);
//处理(数据)
template.process(data, writer);
} catch (IOException | TemplateException e) {
e.printStackTrace();
}
System.out.println("Word文档操作结束!");
}
效果


Excel操作
Excel操作同理,也是先创建一个例子,另存为xml格式,再根据FreeMarker的语法表达式设置值
按照字段名,建Excel例子时直接把取值代码写进去,转成xml后就可能省一点时间


同样,idea打开后格式化一下,遍历设置值

封装一个方法
//Word文档操作
private static void createWord(){
System.out.println("开始Word文档操作...");
//指定TemplateLoading模板根路径
String rootPath = "E:\\Java\\test\\";
//模板文件
String templatePath = "word.xml";
//最终输出文件路径
String outFilePath = rootPath+"word_by_freemarker.docx";
//数据
Map<String, Object> data = new HashMap<>();
data.put("company","某某公司");
data.put("number","0001");
data.put("name","huanzi-qch");
data.put("phone","15600000000");
data.put("department","软件开发部");
data.put("post","开发工程师");
ArrayList<Map<String, String>> works = new ArrayList<>();
Map<String, String> work1 = new HashMap<>();
work1.put("company","某某单位1");
work1.put("time","2018-01 - 2019-01");
work1.put("department","研发1部");
work1.put("post","开发工程师");
works.add(work1);
Map<String, String> work2 = new HashMap<>();
work2.put("company","某某单位2");
work2.put("time","2019-01 - 2020-01");
work2.put("department","研发2部");
work2.put("post","开发工程师");
works.add(work2);
data.put("works",works);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){
//初始化,指定版本与pom文件相同
Configuration configuration = new Configuration(new Version("2.3.31"));
configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString());
configuration.setDirectoryForTemplateLoading(new File(rootPath));
Template template = configuration.getTemplate(templatePath);
//处理(数据)
template.process(data, writer);
} catch (IOException | TemplateException e) {
e.printStackTrace();
}
System.out.println("Word文档操作结束!");
}
效果


代码生成器
代码生成器,先把我们之前写好的JDBC连接数据库工具类、字符串处理工具类、表结构信息实体类以及获取表结构信息的方法先拿过来
/**
* 程序自动设置
*/
private static String tableName;//表名
private static String tableComment;//表注释
/**
* 数据连接相关,需要手动设置
*/
private static final String URL = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
private static final String DRIVER_CLASSNAME = "com.mysql.cj.jdbc.Driver";
/**
* 字符串处理工具类
*/
private static class StringUtil {
/**
* 数据库类型->JAVA类型
*
* @param dbType 数据库类型
* @return JAVA类型
*/
private static String typeMapping(String dbType) {
String javaType;
if ("int|integer".contains(dbType)) {
javaType = "Integer";
} else if ("float|double|decimal|real".contains(dbType)) {
javaType = "Double";
} else if ("date|time|datetime|timestamp".contains(dbType)) {
javaType = "Date";
} else {
javaType = "String";
}
return javaType;
}
/**
* 驼峰转换为下划线
*/
private static String underscoreName(String camelCaseName) {
StringBuilder result = new StringBuilder();
if (camelCaseName != null && camelCaseName.length() > 0) {
result.append(camelCaseName.substring(0, 1).toLowerCase());
for (int i = 1; i < camelCaseName.length(); i++) {
char ch = camelCaseName.charAt(i);
if (Character.isUpperCase(ch)) {
result.append("_");
result.append(Character.toLowerCase(ch));
} else {
result.append(ch);
}
}
}
return result.toString();
}
/**
* 首字母大写
*/
private static String captureName(String name) {
char[] cs = name.toCharArray();
cs[0] -= 32;
return String.valueOf(cs);
}
/**
* 下划线转换为驼峰
*/
private static String camelCaseName(String underscoreName) {
StringBuilder result = new StringBuilder();
if (underscoreName != null && underscoreName.length() > 0) {
boolean flag = false;
for (int i = 0; i < underscoreName.length(); i++) {
char ch = underscoreName.charAt(i);
if ("_".charAt(0) == ch) {
flag = true;
} else {
if (flag) {
result.append(Character.toUpperCase(ch));
flag = false;
} else {
result.append(ch);
}
}
}
}
return result.toString();
}
}
/**
* JDBC连接数据库工具类
*/
private static class DBConnectionUtil {
static {
// 1、加载驱动
try {
Class.forName(DRIVER_CLASSNAME);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 返回一个Connection连接
*/
static Connection getConnection() {
Connection conn = null;
// 2、连接数据库
try {
conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
/**
* 关闭Connection,Statement连接
*/
public static void close(Connection conn, Statement stmt) {
try {
conn.close();
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 关闭Connection,Statement,ResultSet连接
*/
public static void close(Connection conn, Statement stmt, ResultSet rs) {
try {
close(conn, stmt);
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 表结构信息实体类
*/
private static class TableInfo {
private String columnName;//字段名
private String dataType;//字段类型
private String columnComment;//字段注释
private String columnKey;//主键
private String extra;//主键类型
public String getColumnName() {
return columnName;
}
public void setColumnName(String columnName) {
this.columnName = columnName;
}
public String getDataType() {
return dataType;
}
public void setDataType(String dataType) {
this.dataType = dataType;
}
public String getColumnComment() {
return columnComment;
}
public void setColumnComment(String columnComment) {
this.columnComment = columnComment;
}
public String getColumnKey() {
return columnKey;
}
public void setColumnKey(String columnKey) {
this.columnKey = columnKey;
}
public String getExtra() {
return extra;
}
public void setExtra(String extra) {
this.extra = extra;
}
}
/**
* 获取表结构信息
* 目前仅支持mysql
*/
private static List<TableInfo> getTableInfo() {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
ArrayList<TableInfo> list = new ArrayList<>();
try {
conn = DBConnectionUtil.getConnection();
//表字段信息
String sql = "select column_name,data_type,column_comment,column_key,extra from information_schema.columns where table_schema = (select database()) and table_name=?";
ps = conn.prepareStatement(sql);
ps.setString(1, tableName);
rs = ps*ex.e**cuteQuery();
while (rs.next()) {
TableInfo tableInfo = new TableInfo();
//列名,全部转为小写
tableInfo.setColumnName(rs.getString("column_name").toLowerCase());
//列类型
tableInfo.setDataType(rs.getString("data_type"));
//列注释
tableInfo.setColumnComment(rs.getString("column_comment"));
//主键
tableInfo.setColumnKey(rs.getString("column_key"));
//主键类型
tableInfo.setExtra(rs.getString("extra"));
list.add(tableInfo);
}
//表注释
sql = "select table_comment from information_schema.tables where table_schema = (select database()) and table_name=?";
ps = conn.prepareStatement(sql);
ps.setString(1, tableName);
rs = ps*ex.e**cuteQuery();
while (rs.next()) {
//表注释
tableComment = rs.getString("table_comment");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if(rs != null){
DBConnectionUtil.close(conn, ps, rs);
}
}
return list;
}
从原来的代码生成器那里拿一个entity.tlf模板,小改动一下(主要是循环、if判断哪里不一样)
package cn.huanzi.qch.baseadmin.sys.${entityToLowerCase}.pojo;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
/**
* ${tableComment} 实体类
*
* ${author}
* ${date}
*/
@Entity
@Table(name = "${tableName}")
@Data
public class ${entity} implements Serializable {
<#list tableInfos as tableInfo>
<#if tableInfo.columnKey == "PRI">@Id</#if>
<#if tableInfo.extra == "auto_increment">@GeneratedValue(strategy= GenerationType.IDENTITY)</#if>
private ${tableInfo.dataType} ${tableInfo.columnName};//${tableInfo.columnComment}
</#list>
}
封装一个方法
//代码生成
private static void autoGenerator(String tName){
System.out.println("开始代码生成操作...");
tableName = tName;
//指定TemplateLoading模板根路径
String rootPath = "E:\\Java\\test\\";
//模板文件
String templatePath = "entity.tlf";
//最终输出文件路径
String outFilePath = rootPath+"entity.java";
//数据
Map<String, Object> data = new HashMap<>();
//驼峰标识映射后的表名
String captureName = StringUtil.captureName(StringUtil.camelCaseName(tableName));
//获取表信息,并进行处理
List<TableInfo> tableInfoList = getTableInfo();
ArrayList<Map<String, String>> tableInfos = new ArrayList<>();
for (TableInfo info : tableInfoList) {
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("columnName", StringUtil.camelCaseName(info.getColumnName()));
hashMap.put("dataType", StringUtil.typeMapping(info.getDataType()));
hashMap.put("columnComment", info.getColumnComment());
hashMap.put("columnKey", info.getColumnKey());
hashMap.put("extra", info.getExtra());
tableInfos.add(hashMap);
}
data.put("entityToLowerCase",captureName.toLowerCase());
data.put("tableComment",tableComment);
data.put("author","作者:Auto Generator By 'huanzi-qch'");
data.put("date","生成日期:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
data.put("tableName",tableName);
data.put("entity",captureName);
data.put("tableInfos",tableInfos);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){
//初始化,指定版本与pom文件相同
Configuration configuration = new Configuration(new Version("2.3.31"));
configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString());
configuration.setDirectoryForTemplateLoading(new File(rootPath));
Template template = configuration.getTemplate(templatePath);
//处理(数据)
template.process(data, writer);
} catch (IOException | TemplateException e) {
e.printStackTrace();
}
System.out.println("代码生成操作结束!");
}
效果


完整代码
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.Version;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Date;
public class Test {
//Word文档操作
private static void createWord(){
System.out.println("开始Word文档操作...");
//指定TemplateLoading模板根路径
String rootPath = "E:\\Java\\test\\";
//模板文件
String templatePath = "word.xml";
//最终输出文件路径
String outFilePath = rootPath+"word_by_freemarker.docx";
//数据
Map<String, Object> data = new HashMap<>();
data.put("company","某某公司");
data.put("number","0001");
data.put("name","huanzi-qch");
data.put("phone","15600000000");
data.put("department","软件开发部");
data.put("post","开发工程师");
ArrayList<Map<String, String>> works = new ArrayList<>();
Map<String, String> work1 = new HashMap<>();
work1.put("company","某某单位1");
work1.put("time","2018-01 - 2019-01");
work1.put("department","研发1部");
work1.put("post","开发工程师");
works.add(work1);
Map<String, String> work2 = new HashMap<>();
work2.put("company","某某单位2");
work2.put("time","2019-01 - 2020-01");
work2.put("department","研发2部");
work2.put("post","开发工程师");
works.add(work2);
data.put("works",works);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){
//初始化,指定版本与pom文件相同
Configuration configuration = new Configuration(new Version("2.3.31"));
configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString());
configuration.setDirectoryForTemplateLoading(new File(rootPath));
Template template = configuration.getTemplate(templatePath);
//处理(数据)
template.process(data, writer);
} catch (IOException | TemplateException e) {
e.printStackTrace();
}
System.out.println("Word文档操作结束!");
}
//Excel文档操作
private static void createExcel(){
System.out.println("开始Excel文档操作...");
//指定TemplateLoading模板根路径
String rootPath = "E:\\Java\\test\\";
//模板文件
String templatePath = "excel.xml";
//最终输出文件路径
String outFilePath = rootPath+"excel_by_freemarker.xlsx";
//数据
Map<String, Object> data = new HashMap<>();
ArrayList<Map<String, String>> persons = new ArrayList<>();
Map<String, String> person1 = new HashMap<>();
person1.put("name","huanzi-qch1");
person1.put("phone","15600000000");
person1.put("company","某某单位1");
person1.put("time","2018-01 - 2019-01");
person1.put("department","研发1部");
person1.put("post","开发工程师");
persons.add(person1);
Map<String, String> person2 = new HashMap<>();
person2.put("name","huanzi-qch2");
person2.put("phone","15600000000");
person2.put("company","某某单位2");
person2.put("time","2019-01 - 2020-01");
person2.put("department","研发2部");
person2.put("post","开发工程师");
persons.add(person2);
data.put("persons",persons);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){
//初始化,指定版本与pom文件相同
Configuration configuration = new Configuration(new Version("2.3.31"));
configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString());
configuration.setDirectoryForTemplateLoading(new File(rootPath));
Template template = configuration.getTemplate(templatePath);
//处理(数据)
template.process(data, writer);
} catch (IOException | TemplateException e) {
e.printStackTrace();
}
System.out.println("Excel文档操作结束!");
}
//代码生成
private static void autoGenerator(String tName){
System.out.println("开始代码生成操作...");
tableName = tName;
//指定TemplateLoading模板根路径
String rootPath = "E:\\Java\\test\\";
//模板文件
String templatePath = "entity.tlf";
//最终输出文件路径
String outFilePath = rootPath+"entity.java";
//数据
Map<String, Object> data = new HashMap<>();
//驼峰标识映射后的表名
String captureName = StringUtil.captureName(StringUtil.camelCaseName(tableName));
//获取表信息,并进行处理
List<TableInfo> tableInfoList = getTableInfo();
ArrayList<Map<String, String>> tableInfos = new ArrayList<>();
for (TableInfo info : tableInfoList) {
HashMap<String, String> hashMap = new HashMap<>();
hashMap.put("columnName", StringUtil.camelCaseName(info.getColumnName()));
hashMap.put("dataType", StringUtil.typeMapping(info.getDataType()));
hashMap.put("columnComment", info.getColumnComment());
hashMap.put("columnKey", info.getColumnKey());
hashMap.put("extra", info.getExtra());
tableInfos.add(hashMap);
}
data.put("entityToLowerCase",captureName.toLowerCase());
data.put("tableComment",tableComment);
data.put("author","作者:Auto Generator By 'huanzi-qch'");
data.put("date","生成日期:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
data.put("tableName",tableName);
data.put("entity",captureName);
data.put("tableInfos",tableInfos);
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFilePath), StandardCharsets.UTF_8));){
//初始化,指定版本与pom文件相同
Configuration configuration = new Configuration(new Version("2.3.31"));
configuration.setDefaultEncoding(StandardCharsets.UTF_8.toString());
configuration.setDirectoryForTemplateLoading(new File(rootPath));
Template template = configuration.getTemplate(templatePath);
//处理(数据)
template.process(data, writer);
} catch (IOException | TemplateException e) {
e.printStackTrace();
}
System.out.println("代码生成操作结束!");
}
/**
* 程序自动设置
*/
private static String tableName;//表名
private static String tableComment;//表注释
/**
* 数据连接相关,需要手动设置
*/
private static final String URL = "jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8";
private static final String USERNAME = "root";
private static final String PASSWORD = "123456";
private static final String DRIVER_CLASSNAME = "com.mysql.cj.jdbc.Driver";
/**
* 字符串处理工具类
*/
private static class StringUtil {
/**
* 数据库类型->JAVA类型
*
* @param dbType 数据库类型
* @return JAVA类型
*/
private static String typeMapping(String dbType) {
String javaType;
if ("int|integer".contains(dbType)) {
javaType = "Integer";
} else if ("float|double|decimal|real".contains(dbType)) {
javaType = "Double";
} else if ("date|time|datetime|timestamp".contains(dbType)) {
javaType = "Date";
} else {
javaType = "String";
}
return javaType;
}
/**
* 驼峰转换为下划线
*/
private static String underscoreName(String camelCaseName) {
StringBuilder result = new StringBuilder();
if (camelCaseName != null && camelCaseName.length() > 0) {
result.append(camelCaseName.substring(0, 1).toLowerCase());
for (int i = 1; i < camelCaseName.length(); i++) {
char ch = camelCaseName.charAt(i);
if (Character.isUpperCase(ch)) {
result.append("_");
result.append(Character.toLowerCase(ch));
} else {
result.append(ch);
}
}
}
return result.toString();
}
/**
* 首字母大写
*/
private static String captureName(String name) {
char[] cs = name.toCharArray();
cs[0] -= 32;
return String.valueOf(cs);
}
/**
* 下划线转换为驼峰
*/
private static String camelCaseName(String underscoreName) {
StringBuilder result = new StringBuilder();
if (underscoreName != null && underscoreName.length() > 0) {
boolean flag = false;
for (int i = 0; i < underscoreName.length(); i++) {
char ch = underscoreName.charAt(i);
if ("_".charAt(0) == ch) {
flag = true;
} else {
if (flag) {
result.append(Character.toUpperCase(ch));
flag = false;
} else {
result.append(ch);
}
}
}
}
return result.toString();
}
}
/**
* JDBC连接数据库工具类
*/
private static class DBConnectionUtil {
static {
// 1、加载驱动
try {
Class.forName(DRIVER_CLASSNAME);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 返回一个Connection连接
*/
static Connection getConnection() {
Connection conn = null;
// 2、连接数据库
try {
conn = DriverManager.getConnection(URL, USERNAME, PASSWORD);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
/**
* 关闭Connection,Statement连接
*/
public static void close(Connection conn, Statement stmt) {
try {
conn.close();
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 关闭Connection,Statement,ResultSet连接
*/
public static void close(Connection conn, Statement stmt, ResultSet rs) {
try {
close(conn, stmt);
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 表结构信息实体类
*/
private static class TableInfo {
private String columnName;//字段名
private String dataType;//字段类型
private String columnComment;//字段注释
private String columnKey;//主键
private String extra;//主键类型
public String getColumnName() {
return columnName;
}
public void setColumnName(String columnName) {
this.columnName = columnName;
}
public String getDataType() {
return dataType;
}
public void setDataType(String dataType) {
this.dataType = dataType;
}
public String getColumnComment() {
return columnComment;
}
public void setColumnComment(String columnComment) {
this.columnComment = columnComment;
}
public String getColumnKey() {
return columnKey;
}
public void setColumnKey(String columnKey) {
this.columnKey = columnKey;
}
public String getExtra() {
return extra;
}
public void setExtra(String extra) {
this.extra = extra;
}
}
/**
* 获取表结构信息
* 目前仅支持mysql
*/
private static List<TableInfo> getTableInfo() {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
ArrayList<TableInfo> list = new ArrayList<>();
try {
conn = DBConnectionUtil.getConnection();
//表字段信息
String sql = "select column_name,data_type,column_comment,column_key,extra from information_schema.columns where table_schema = (select database()) and table_name=?";
ps = conn.prepareStatement(sql);
ps.setString(1, tableName);
rs = ps*ex.e**cuteQuery();
while (rs.next()) {
TableInfo tableInfo = new TableInfo();
//列名,全部转为小写
tableInfo.setColumnName(rs.getString("column_name").toLowerCase());
//列类型
tableInfo.setDataType(rs.getString("data_type"));
//列注释
tableInfo.setColumnComment(rs.getString("column_comment"));
//主键
tableInfo.setColumnKey(rs.getString("column_key"));
//主键类型
tableInfo.setExtra(rs.getString("extra"));
list.add(tableInfo);
}
//表注释
sql = "select table_comment from information_schema.tables where table_schema = (select database()) and table_name=?";
ps = conn.prepareStatement(sql);
ps.setString(1, tableName);
rs = ps*ex.e**cuteQuery();
while (rs.next()) {
//表注释
tableComment = rs.getString("table_comment");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if(rs != null){
DBConnectionUtil.close(conn, ps, rs);
}
}
return list;
}
public static void main(String[] args) {
// createWord();
// createExcel();
// autoGenerator("tb_user");
}
}
后记
通过FreeMarker,按照固定格式,快速生成Word、Excel文档,或者生成代码,简单高效。
生成文档,数据来源可能是直接读库获取,也有可能是要用户在页面上填写,再传入后台,这时候就可以将我们的模板文件,另存为html格式,小调整之后就可以展示给用户,最大程度保证了用户看到的文档页面跟生成、导出的文档格式是一致的。
版权声明
作者:huanzi-qch
出处:https://www.cnblogs.com/huanzi-qch
若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.