最近接到一个需求:快速生成一份包含文字、图片和数据报表的 Word 报告,而且图表数据要能灵活调整。一开始想到的是 Apache POI,但翻了不少资料后,发现 poi-tl 这个基于 Apache POI 的模板引擎更适合手头的业务场景——上手简单、代码量也少。于是花时间研究了一轮,顺手把常用的标签封装成了一个工具类,方便后续快速复用。
话不多说,先看 poi-tl 到底是什么。
1、POI-TL 简介
poi-tl 是一个免费开源的 Java 类库,基于 Apache POI 实现,本质上是一个纯 Java 组件模板引擎。它跨平台、代码短小精悍,通过插件机制具备高度扩展性。
使用方式和 Freemarker 类似,需要预先设置好模板,但整体更简单、也更容易上手。
2、常用标签
2.1 文本
格式:{{var}}
数据模型:
| 类型 |
描述 |
| String |
纯文本 |
| TextRenderData |
带有样式的文本 |
| HyperLinkTextRenderData |
超链接文本 |
| Object |
调用 toString() 方法转化成文本 |
例如:
put("name", "Sayi");
put("author", new TextRenderData("000000", "Sayi"));
// 超链接
put("link", new HyperLinkTextRenderData("website", "http://deepoove.com"));
2.2 图片
格式:以 @ 开始,{{@var}}
数据模型:
| 类型 |
描述 |
| PictureRenderData |
支持本地图片,也支持远程 URL 地址(将 URL 图片转成 BufferedImage) |
例如:
// 本地图片
put("local", new PictureRenderData(80, 100, "./sayi.png"));
// 图片流
put("localbyte", new PictureRenderData(80, 100, ".png", new FileInputStream("./logo.png")));
// 网络图片(注意网络耗时对系统可能的性能影响)
put("urlpicture", new PictureRenderData(50, 50, ".png",
BytePictureUtils.getUrlBufferedImage("http://deepoove.com/images/icecream.png")));
2.3 表格
poi-tl 默认实现了 N 行 N 列的表格样式,同时提供了数据为空时展示一行空数据的文案。
格式:以 # 开头,{{#var}}
数据模型:
| 类型 |
描述 |
| MiniTableRenderData |
主要调用 TextRenderData,提供样式等 |
2.4 列表
格式:以 * 开头,{{*var}}
数据模型:
| 类型 |
描述 |
| NumbericRenderData |
主要调用 TextRenderData,提供样式等 |
NumbericRenderData 支持多种列表样式,包括罗马字符、有序无序等:
FMT_DECIMAL // 1. 2. 3.
FMT_DECIMAL_PARENTHESES // 1) 2) 3)
FMT_BULLET // ● ● ●
FMT_LOWER_LETTER // a. b. c.
FMT_LOWER_ROMAN // i ⅱ ⅲ
FMT_UPPER_LETTER // A. B. C.
2.5 单系列图表
单系列图表,指的是图形中只展示一列数据,例如单数据的柱状图、饼图等。
格式:先创建单系列图,然后在图表区格式 -> 可选文字 -> 标题,与文字一样,以 {{val}} 命名,如图:

数据模型:
| 类型 |
描述 |
| ChartSingleSeriesRenderData |
提供了设置标题、类别等方法 |
例如:
ChartSingleSeriesRenderData singleSeriesRenderData = new ChartSingleSeriesRenderData();
singleSeriesRenderData.setCategories(new String[] { "俄罗斯", "加拿大", "美国", "中国" });
singleSeriesRenderData.setChartTitle("测试");
pie.setSeriesData(new SeriesRenderData("countries", new Integer[] { 17098242, 9984670, 9826675, 9596961 }));
2.6 多系列图表
在报表应用中,很多时候使用的是多系列组合,例如柱状图与折线图组合等。
格式与单系列一致:

数据模型:
| 类型 |
描述 |
| ChartMultiSeriesRenderData |
提供了设置标题、类别等方法 |
例如:
ChartMultiSeriesRenderData chart = new ChartMultiSeriesRenderData();
chart.setChartTitle("MyChart");
chart.setCategories(new String[] { "中文", "English" });
List<SeriesRenderData> seriesRenderData = new ArrayList<>();
seriesRenderData.add(new SeriesRenderData("countries", new Double[] { 15.0, 6.0 }));
seriesRenderData.add(new SeriesRenderData("speakers", new Double[] { 223.0, 119.0 }));
chart.setSeriesDatas(seriesRenderData);
3、代码封装
以上介绍了几种常用标签,更多标签可以参考官方文档。既然已经清楚了标签的用法,接下来做一轮代码整合——封装一个工具类,快速实现多标签一起生成报表。
引入 jar 包,以 1.8.2 版本为例:
<dependency>
<groupId>com.deepoove</groupId>
<artifactId>poi-tl</artifactId>
<version>1.8.2</version>
</dependency>
3.1 创建标签类型
标签类型用于声明程序支持哪些标签,例如文字、图片等。使用枚举实现,方便后期扩展。
/**
* @author: jiangjs
* @description: 标签类型
**/
public enum WordContentTypeEnum {
/**
* 文本
*/
TEXT,
/**
* 图片
*/
PICTURE,
/**
* 表格
*/
TABLE,
/**
* 列表
*/
LIST,
/**
* 图表
*/
CHART;
}
3.2 创建公共实体
该实体用于描述替换的标签名称及标签类型。标签名称即 put 使用的名称,与 Word 模板中定义的名称一致。
/**
* @author: jiangjs
* @description: 公共实体
* @date: 2022/11/24 15:05
**/
@Data
@Accessors(chain = true)
public class LabelData {
/**
* 标签名称,即 put 使用到的名称
*/
private String labelName;
/**
* 文件内容类型
*/
private WordContentTypeEnum typeEnum;
}
3.3 封装统一数据生成接口
该接口只提供各标签数据生成的封装,返回一个 Object。
/**
* @author: jiangjs
* @description: 封装统一各标签数据生成接口
**/
public interface GenerateWord {
Object generateWord(LabelData data);
}
3.4 创建工厂
主要用于对各标签生成的数据进行统一管理。
/**
* @author: jiangjs
* @description: 生成word工厂
**/
public class GenerateWordFactory {
private static final Map<WordContentTypeEnum, GenerateWord> TYPE_BACK_DATA = new HashMap<>();
public static void register(WordContentTypeEnum typeEnum, GenerateWord word) {
if (Objects.nonNull(typeEnum)) {
TYPE_BACK_DATA.put(typeEnum, word);
}
}
public static GenerateWord getBackData(WordContentTypeEnum typeEnum) {
return TYPE_BACK_DATA.get(typeEnum);
}
}
TYPE_BACK_DATA:各标签封装数据的类与标签类型的一一对应关系。
GenerateWord getBackData(WordContentTypeEnum typeEnum):根据标签类型获取对应的数据生成方法。
3.5 封装 Word 生成类
这个类的主要作用是将封装的各标签数据写入 Word 模板,并生成最终报表。
/**
* @author: jiangjs
* @description: 操作word内容
**/
public class OperateWordManage {
private final static Logger log = LoggerFactory.getLogger(OperateWordManage.class);
public static void generateWordContent(File tempFileFile, String destFilePath, List<LabelData> contents) {
FileOutputStream fos = null;
XWPFTemplate template = null;
try {
template = XWPFTemplate.compile(tempFileFile).render(new HashMap<String, Object>(contents.size()) {{
contents.forEach(content -> {
GenerateWord backData = GenerateWordFactory.getBackData(content.getTypeEnum());
put(content.getLabelName(), backData.generateWord(content));
});
}});
fos = new FileOutputStream(destFilePath);
template.write(fos);
fos.flush();
} catch (Exception e) {
log.error("替换生成图表报错:{}", e.getMessage());
e.printStackTrace();
} finally {
try {
if (Objects.nonNull(fos)) {
fos.close();
}
if (Objects.nonNull(template)) {
template.close();
}
} catch (Exception e) {
log.error("关闭数据流报错:{}", e.getMessage());
e.printStackTrace();
}
}
}
}
tempFileFile:模板文件地址。
destFilePath:生成后的文件地址。
List<LabelData> contents:各标签封装后的数据集合。
3.6 各标签生成数据封装
3.6.1 文本封装
3.6.1.1 创建文本实体
文本实体涵盖纯文本、带样式文本、超链接文本三类。
/**
* @author: jiangjs
* @description: 文本
**/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class TextContentData extends LabelData {
/**
* 纯文本内容
*/
private String content;
/**
* 带样式文本
*/
private TextRenderData renderData;
/**
* 超链接文本
*/
private HyperLinkTextRenderData linkData;
}
3.6.1.2 创建数据封装类
该封装类根据不同的属性值返回不同类型,优先返回超链接,其次是带样式的文本,最后是纯文本。
/**
* @author: jiangjs
* @description: 文本内容实现
**/
@Component
public class TextGenerateWord implements GenerateWord {
@PostConstruct
public void init() {
GenerateWordFactory.register(WordContentTypeEnum.TEXT, this);
}
@Override
public Object generateWord(LabelData data) {
TextContentData contentData = (TextContentData) data;
return Objects.nonNull(contentData.getLinkData()) ? contentData.getLinkData() :
Objects.nonNull(contentData.getRenderData()) ? contentData.getRenderData() :
contentData.getContent();
}
}
每个数据封装方法都有一个初始化方法,主要作用是调用工厂注册方法,将标签类型与当前数据生成方法绑定。
3.6.2 图片封装
3.6.2.1 创建图片实体
/**
* @author: jiangjs
* @description: 图片
**/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class PictureContentData extends LabelData {
/**
* 图片宽度
*/
private Integer width;
/**
* 图片高度
*/
private Integer height;
/**
* 图片类型
*/
private PicTypeEnum picType;
/**
* 图片地址(网络图片插入时使用)
*/
private String picUrl;
/**
* 图片文件
*/
private File file;
}
PicTypeEnum:图片类型。
/**
* @author: jiangjs
* @description: 图片类型
**/
public enum PicTypeEnum {
/**
* png图片
*/
PNG(".png"),
/**
* JPG图片
*/
JPG(".jpg"),
/**
* jpeg
*/
JPEG(".jpeg");
private final String picName;
PicTypeEnum(String picName) {
this.picName = picName;
}
public String getPicName() {
return picName;
}
}
3.6.2.2 创建数据封装类
/**
* @author: jiangjs
* @description: 图片内容实现
**/
@Component
public class PictureGenerateWord implements GenerateWord {
@PostConstruct
private void init() {
GenerateWordFactory.register(WordContentTypeEnum.PICTURE, this);
}
@Override
public Object generateWord(LabelData data) {
PictureContentData picture = (PictureContentData) data;
return StringUtils.isNotBlank(picture.getPicUrl()) ?
new PictureRenderData(picture.getWidth(), picture.getHeight(), picture.getPicType().getPicName(),
BytePictureUtils.getUrlBufferedImage(picture.getPicUrl()))
: new PictureRenderData(picture.getWidth(), picture.getHeight(), picture.getFile());
}
}
该封装会判断图片实体中是否包含图片链接,从而创建对应的数据对象。
3.6.3 表格封装
3.6.3.1 创建表格实体
/**
* @author: jiangjs
* @description: 表格
**/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class TableSeriesRenderData extends LabelData {
/**
* 表头
*/
private TextRenderData[] header;
/**
* 表内容
*/
private List<TextRenderData[]> contents;
}
将表头与表格内容分开赋值,使结构更清晰。
3.6.3.2 创建数据封装类
/**
* @author: jiangjs
* @description: 表格内容实现
**/
@Component
public class TableGenerateWord implements GenerateWord {
@PostConstruct
private void init() {
GenerateWordFactory.register(WordContentTypeEnum.TABLE, this);
}
@Override
public Object generateWord(LabelData data) {
TableSeriesRenderData tableData = (TableSeriesRenderData) data;
RowRenderData header = RowRenderData.build(tableData.getHeader());
List<RowRenderData> contentData = new ArrayList<>();
tableData.getContents().forEach(con -> {
contentData.add(RowRenderData.build(con));
});
return new MiniTableRenderData(header, contentData);
}
}
3.6.4 列表封装
3.6.4.1 创建列表实体
/**
* @author: jiangjs
* @description: 列表
**/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class ListRenderData extends LabelData {
/**
* 列表数据集
*/
private List<TextRenderData> list;
/**
* 列表样式,支持罗马字符、有序无序等,默认为点
*/
private Pair<STNumberFormat.Enum, String> pair = NumbericRenderData.FMT_BULLET;
}
3.6.4.2 创建数据封装类
/**
* @author: jiangjs
* @description: 列表内容实现
**/
@Component
public class ListGenerateWord implements GenerateWord {
@PostConstruct
private void init() {
GenerateWordFactory.register(WordContentTypeEnum.LIST, this);
}
@Override
public Object generateWord(LabelData data) {
ListRenderData listData = (ListRenderData) data;
return new NumbericRenderData(listData.getPair(), listData.getList());
}
}
3.6.5 图表封装
3.6.5.1 创建图表实体
/**
* @author: jiangjs
* @description: 图表
**/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class ChartSeriesRenderData extends LabelData {
/**
* 横轴数据
*/
private String[] categories;
/**
* 图表名称
*/
private String title;
/**
* 图表类型 组合
*/
private CharCombinationType charType = CharCombinationType.MULTI;
/**
* 系列对应数据
*/
private List<RenderData> senderData;
@Data
public static class RenderData {
/**
* 系列名称
*/
private String renderTitle;
/**
* 系列对应的数据
*/
private Number[] data;
/**
* 该系列对应生成的图表类型
*/
private SeriesRenderData.ComboType comboType = null;
}
}
CharCombinationType 表示图表中系列的类型,只有单系列或多系列两种。
/**
* @author: jiangjs
* @description: 图表系列类型
**/
public enum CharCombinationType {
/**
* 多组合
*/
MULTI("Multi"),
/**
* 单图形
*/
Single("Single");
private final String type;
CharCombinationType(String type) {
this.type = type;
}
public String getType() {
return type;
}
}
3.6.5.2 创建数据封装类
/**
* @author: jiangjs
* @description: 图表类型
**/
@Component
public class ChartGenerateWord implements GenerateWord {
@PostConstruct
private void init() {
GenerateWordFactory.register(WordContentTypeEnum.CHART, this);
}
@Override
public Object generateWord(LabelData obj) {
ChartSeriesRenderData renderData = (ChartSeriesRenderData) obj;
if (Objects.nonNull(renderData.getCharType()) && Objects.equals("Single", renderData.getCharType().getType())) {
ChartSingleSeriesRenderData singleSeriesRenderData = new ChartSingleSeriesRenderData();
singleSeriesRenderData.setCategories(renderData.getCategories());
singleSeriesRenderData.setChartTitle(renderData.getTitle());
ChartSeriesRenderData.RenderData seriesData = renderData.getSenderData().get(0);
SeriesRenderData srd = new SeriesRenderData(seriesData.getRenderTitle(), seriesData.getData());
if (Objects.nonNull(seriesData.getComboType())) {
srd.setComboType(seriesData.getComboType());
}
singleSeriesRenderData.setSeriesData(srd);
return singleSeriesRenderData;
} else {
ChartMultiSeriesRenderData seriesRenderData = new ChartMultiSeriesRenderData();
seriesRenderData.setCategories(renderData.getCategories());
seriesRenderData.setChartTitle(renderData.getTitle());
List<ChartSeriesRenderData.RenderData> renderDataList = renderData.getSenderData();
List<SeriesRenderData> groupData = new ArrayList<>();
renderDataList.forEach(data -> {
SeriesRenderData srd = new SeriesRenderData(data.getRenderTitle(), data.getData());
if (Objects.nonNull(data.getComboType())) {
srd.setComboType(data.getComboType());
}
groupData.add(srd);
});
seriesRenderData.setSeriesDatas(groupData);
return seriesRenderData;
}
}
}
代码会根据系列类型的不同,创建不同的数据类型返回。
至此,常用标签的数据封装已经完成,进入测试环节。
4、测试
4.1 创建文件模板
根据前面介绍的常用标签,在 Word 中创建文件模板,如图:

将模板放在项目的 resources 目录下:

4.2 封装各标签数据
根据封装后的标签数据生成类来进行数据组织。
private static final String TEMPLATE_PATH = "static/template/demo_template.docx";
public void generateCharts() {
File templateFile = null;
try {
templateFile = new ClassPathResource(TEMPLATE_PATH).getFile();
} catch (IOException e) {
e.printStackTrace();
}
List<LabelData> generates = new ArrayList<>();
// 文本
TextContentData contentData = new TextContentData();
contentData.setContent("2022年月通报函生成报告").setLabelName("title").setTypeEnum(WordContentTypeEnum.TEXT);
generates.add(contentData);
// 带样式文本
TextContentData typeData = new TextContentData();
typeData.setRenderData(new TextRenderData("cc0000", "这是带样式的内容"))
.setLabelName("typeContent").setTypeEnum(WordContentTypeEnum.TEXT);
generates.add(typeData);
// 插入图片
PictureContentData picData = new PictureContentData();
picData.setWidth(200).setHeight(160).setPicType(PicTypeEnum.JPG)
.setFile(new File("D:\\down\\java.jpg"))
.setLabelName("picture").setTypeEnum(WordContentTypeEnum.PICTURE);
generates.add(picData);
// 插入表格
TableSeriesRenderData tableData = new TableSeriesRenderData();
List<TextRenderData[]> contents = Arrays.asList(
new TextRenderData[]{new TextRenderData("科教1班"), new TextRenderData("1")},
new TextRenderData[]{new TextRenderData("幼儿3班"), new TextRenderData("6")});
tableData.setHeader(new TextRenderData[]{new TextRenderData("班级"), new TextRenderData("排名")})
.setContents(contents).setLabelName("showTable").setTypeEnum(WordContentTypeEnum.TABLE);
generates.add(tableData);
// 插入列表
ListRenderData listRenderData = new ListRenderData();
List<TextRenderData> listData = Arrays.asList(
new TextRenderData("排序1"),
new TextRenderData("排序2"),
new TextRenderData("排序3"));
listRenderData.setList(listData).setPair(NumbericRenderData.FMT_LOWER_ROMAN)
.setTypeEnum(WordContentTypeEnum.LIST).setLabelName("numList");
generates.add(listRenderData);
// 折线
ChartSeriesRenderData lineData = new ChartSeriesRenderData();
List<ChartSeriesRenderData.RenderData> lineRenderData = new ArrayList<>();
ChartSeriesRenderData.RenderData numRenderData = new ChartSeriesRenderData.RenderData();
ChartSeriesRenderData.RenderData moneyRenderData = new ChartSeriesRenderData.RenderData();
numRenderData.setRenderTitle("项目数量").setData(new Double[] {
-11.02, -19.42, -10.61, -11.41, -7.91, -5.44, -5.30, -2.75, -1.24, 0.35});
moneyRenderData.setRenderTitle("投资额").setData(new Number[] {
-12.66, -19.41, -15.16, -19.72, -17.05, -15.92, -15.10, -13.04, -10.65, -9.15});
lineRenderData.add(numRenderData);
lineRenderData.add(moneyRenderData);
lineData.setTitle("1-10月份全国新开工项目数量、投资额增速")
.setCategories(new String[] {"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月"})
.setSenderData(lineRenderData).setTypeEnum(WordContentTypeEnum.CHART).setLabelName("speedLine");
generates.add(lineData);
// 柱状图
ChartSeriesRenderData barData = new ChartSeriesRenderData();
List<ChartSeriesRenderData.RenderData> barRenderData = new ArrayList<>();
ChartSeriesRenderData.RenderData openRenderData = new ChartSeriesRenderData.RenderData();
ChartSeriesRenderData.RenderData moneyData = new ChartSeriesRenderData.RenderData();
openRenderData.setRenderTitle("开工数量").setData(new Number[] {
40, 50, 45, 12, 21, 18, 21, 28, 21, 18, 28, 18, 20, 19,
-10, -9, -10, 19, 39, 31, 20, 19, -10, -9, -10, 19, 39, 31,
-10, 19, 39});
moneyData.setRenderTitle("投资额").setData(new Number[] {
20, -22, -12, 8, -10, -14, -10, -10, -8, -2, -8, -1, -9, -21,
-9, -7, -21, -10, 21, -29, -50, -21, -9, -7, -21, -10, 21, -29,
-21, -10, 21});
barRenderData.add(openRenderData);
barRenderData.add(moneyData);
barData.setTitle("各省(自治区)直辖市新开项目数量、投资额同比情况")
.setCategories(new String[] {
"贵州", "西藏", "黑龙江", "浙江", "湖北", "江苏", "四川", "福建", "安徽", "海南",
"山西", "广西", "青海", "广东", "甘肃", "云南", "宁夏", "新疆", "湖南", "北京",
"河北", "山西", "山东", "内蒙古", "天津", "江西", "吉林", "河南", "重庆", "上海",
"辽宁"})
.setSenderData(barRenderData).setTypeEnum(WordContentTypeEnum.CHART).setLabelName("investmentRatio");
generates.add(barData);
// 生成饼图
ChartSeriesRenderData areaData = new ChartSeriesRenderData();
List<ChartSeriesRenderData.RenderData> areaRenderDatas = new ArrayList<>();
ChartSeriesRenderData.RenderData areaRenderData = new ChartSeriesRenderData.RenderData();
areaRenderData.setData(new Number[]{17098242, 9984670, 9826675, 9596961}).setRenderTitle("投资额")
.setComboType(SeriesRenderData.ComboType.AREA);
areaRenderDatas.add(areaRenderData);
areaData.setTitle("国家投资额").setSenderData(areaRenderDatas).setCharType(CharCombinationType.Single)
.setCategories(new String[]{"俄罗斯", "加拿大", "美国", "中国"})
.setLabelName("areaShow").setTypeEnum(WordContentTypeEnum.CHART);
generates.add(areaData);
// 横向柱状图
ChartSeriesRenderData lateralData = new ChartSeriesRenderData();
List<ChartSeriesRenderData.RenderData> lateralRenderData = new ArrayList<>();
ChartSeriesRenderData.RenderData lateralYearData = new ChartSeriesRenderData.RenderData();
ChartSeriesRenderData.RenderData lateralMoneyData = new ChartSeriesRenderData.RenderData();
lateralYearData.setRenderTitle("2021年").setData(new Number[]{400, 200});
lateralMoneyData.setRenderTitle("2022年").setData(new Number[]{456, 255});
lateralRenderData.add(lateralYearData);
lateralRenderData.add(lateralMoneyData);
lateralData.setTitle("工程建设项目建设周期同比情况")
.setCategories(new String[]{"从立项到开工的用时", "从开工到验收的用时"})
.setSenderData(lateralRenderData).setTypeEnum(WordContentTypeEnum.CHART).setLabelName("cycleRadio");
generates.add(lateralData);
// 组合图表
ChartSeriesRenderData groupData = new ChartSeriesRenderData();
List<ChartSeriesRenderData.RenderData> groupRenderData = new ArrayList<>();
ChartSeriesRenderData.RenderData unOpenData = new ChartSeriesRenderData.RenderData();
ChartSeriesRenderData.RenderData openRadioData = new ChartSeriesRenderData.RenderData();
unOpenData.setComboType(SeriesRenderData.ComboType.BAR).setRenderTitle("未开工项目数(个)")
.setData(new Number[]{
55, 35, 23, 76, 60, 65.1, 70.2, 75.3, 80.4, 85.5, 90.6, 95.7, 26,
76, 60, 65.1, 70.2, 75.3, 80.4, 95.7, 26, 76, 60, 65.1, 70.2, 75.3,
95.7, 26, 76, 60, 65.1});
openRadioData.setComboType(SeriesRenderData.ComboType.LINE).setRenderTitle("开工率(%)")
.setData(new Number[]{
34, 45, 23, 67, 34, 45, 23, 67, 34, 45, 23, 67, 23, 67, 34, 45,
23, 45, 23, 67, 23, 67, 34, 45, 23, 45, 67, 23, 67, 34, 45});
groupRenderData.add(unOpenData);
groupRenderData.add(openRadioData);
groupData.setTitle("各省(区、市)签约项目开工情况")
.setCategories(new String[]{
"北京", "吉林", "云南", "上海", "安徽", "浙江", "江西", "四川", "陕西", "甘肃",
"江苏", "广西", "内蒙古", "福建", "天津", "海南", "黑龙江", "贵州", "山东", "河北",
"辽宁", "湖北", "宁夏", "广东", "重庆", "河南", "新疆", "山西", "湖南", "青海",
"兵团"})
.setSenderData(groupRenderData).setTypeEnum(WordContentTypeEnum.CHART).setLabelName("openCondition");
generates.add(groupData);
// 生成 Word
OperateWordManage.generateWordContent(templateFile, "D:\\down\\output.docx", generates);
}
4.3 生成报表

从生成的 Word 中可以看到,模板中的数据已经被替换,图表也在 Word 中成功生成。
上述就是使用 poi-tl 生成 Word 报表的完整过程,并对其做了封装方便复用。工欲善其事,必先利其器,有时封装是必要的,希望这个做法能给你带来一些启发。
源码地址:https://github.com/lovejiashn/generate_report.git