前段时间做了一个需求:需要快速生成一份数据报告,里面包含了文字、图片和数据报表,同时生成的图形数据也可以随意修改。之前想着使用Apache POI来进行实现,在翻阅一些资料后,发现poi-tl更适合我们的业务,也更容易上手,于是对其进行了研究、也对其中的一些标签进行了封装,形成一个快速实现的工具类。
废话不多说,直接开撸。
poi-tl是一个免费开源的Java类库,是基于Apache POI的模板引擎,纯Java组件,跨平台,代码短小精悍,通过插件机制使其具有高度扩展性。
因此在使用的时候需要实现设置好模板,就像Freemarker一样,但是比其简单,也易操作。
格式:{{var}}
数据模型:
类型 | 描述 |
String | 纯文本 |
TextRenderData | 带有样式的文本 |
HyperLinkTextRenderData | 超链接文本 |
Object | 调用toString()方法转化成文本 |
例如:
java复制代码put("name", "Sayi"); put("author", new TextRenderData("000000", "Sayi")); // 超链接 put("link", new HyperLinkTextRenderData("website", "http://deepoove.com"));
格式:以@开始,{{@var}}
数据模型:
类型 | 描述 |
PictureRenderData | 可以支持本地图片、也可以支持远程的URL地址(主要讲URL图片转成BufferedImage) |
例如:
java复制代码/ 本地图片 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")));
poi-tl默认实现了N行N列的样式(如下图),同时提供了当数据为空时,展示一行空数据的文案。
格式:以#开头,{{#var}}
数据模型:
类型 | 描述 |
MiniTableRenderData | 该类主要也是调用TextRenderData,提供样式等 |
格式:以*开头,{{*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.
单系列图标,是指在图形中只展示一列数据,例如:单数据的柱状图,饼图等。
格式:先创建单系列图,然后在图表区格式 ->可选文字->标题。与文字一样,以{{val}},如图:
数据模型:
类型 | 描述 |
ChartSingleSeriesRenderData | 该类提供了设置标题、类别等 |
例如:
java复制代码ChartSingleSeriesRenderData singleSeriesRenderData = new ChartSingleSeriesRenderData(); singleSeriesRenderData.setCategories(new String[] { "俄罗斯", "加拿大", "美国", "中国" }"); singleSeriesRenderData.setChartTitle("测试"); pie.setSeriesData(new SeriesRenderData("countries", new Integer[] { 17098242, 9984670, 9826675, 9596961 }));
在报表应用中,很多时候使用的是多系列组合,例如:柱状图与折线图组合等。
格式:与单系列一致。
数据模型:
类型 | 描述 |
ChartMultiSeriesRenderData | 该类提供了设置标题、类别等 |
例如:
java复制代码ChartMultiSeriesRenderData chart = new ChartMultiSeriesRenderData(); chart.setChartTitle("MyChart"); chart.setCategories(new String[] { "中文", "English" }); ListseriesRenderData = 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);
上述我们介绍了几种常用标签,更多的标签大家可以参考官方网站。
既然我们已经知道标签,那我们来进行代码的整合,主要是封装一个工具类,快速实现多标签一起生成报表。
引入jar包,我们以1.8.2版本为例:
xml复制代码com.deepoove poi-tl1.8.2
标签类型指程序中支持哪些标签,例如:文字,图片等。使用一个枚举来实现,方便后期进行扩展。
java复制代码/** * @author: jiangjs * @description: 标签类型 * @date: 2022/11/24 13:44 **/ public enum WordContentTypeEnum { /** * 文本 */ TEXT, /** * 图片 */ PICTURE, /** * 表格 */ TABLE, /** * 列表 */ LIST, /** * 图表 */ CHART; }
该实体提供替换的标签名称及标签类型。标签名称及put使用的名称,与word中定义的名称一致。
java复制代码/** * @author: jiangjs * @description: 公共实体 * @date: 2022/11/24 15:05 **/ @Data @Accessors(chain = true) public class LabelData { /** * 标签名称,即put使用到的名称 */ private String labelName; /** * 文件内容类型 */ private WordContentTypeEnum typeEnum; }
该接口只提供各标签数据生成的封装,返回一个Object。
java复制代码/** * @author: jiangjs * @description: 封装统一各标签数据生成接口 * @date: 2022/11/24 13:55 **/ public interface GenerateWord { Object generateWord(LabelData data); }
主要是便于对各标签生成的数据进行管理。
java复制代码/** * @author: jiangjs * @description: 生成word工厂 * @date: 2022/11/24 14:06 **/ public class GenerateWordFactory { private static final MapTYPE_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) :根据标签类型获取对应生成的数据。
这个类的主要作用就是将封装的各标签数据写入到word模板中,并形成最终的报表。
java复制代码/** * @author: jiangjs * @description: 操作word内容 * @date: 2022/11/24 11:32 **/ public class OperateWordManage { private final static Logger log = LoggerFactory.getLogger(OperateWordManage.class); public static void generateWordContent(File tempFileFile, String destFilePath,Listcontents){ FileOutputStream fos = null; XWPFTemplate template = null; try { template = XWPFTemplate.compile(tempFileFile).render(new HashMap (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(); } } } }
tempFilePath:模板文件的地址。
destFilePath:生成后的文件地址。
List
文本实体包括了纯文本、带样式文本、超链接文本。
java复制代码/** * @author: jiangjs * @description: 文本 * @date: 2022/11/24 15:07 **/ @EqualsAndHashCode(callSuper = true) @Data @Accessors(chain = true) public class TextContentData extends LabelData { /** * 纯文本内容 */ private String content; /** * 带样式文本 */ private TextRenderData renderData; /** * 超链接文本 */ private HyperLinkTextRenderData linkData; }
该封装类会根据不同的属性值来返回不同类型。优先返回超链接,其次是带样式的文本,最后是纯文本。
java复制代码/** * @author: jiangjs * @description: 文本内容实现 * @date: 2022/11/24 14:28 **/ @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(); } }
每一个封装数据方法都有一个初始化方法,主要是调用工厂方法将类型与当前数据生成方法进行绑定。
java复制代码/** * @author: jiangjs * @description: 图片 * @date: 2022/11/24 16:34 **/ @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:图片类型。
java复制代码/** * @author: jiangjs * @description: 图片类型 * @date: 2022/11/24 16:50 **/ 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; } }
java复制代码/** * @author: jiangjs * @description: * @date: 2022/11/24 16:46 **/ @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()); } }
该封装会根据图片实体中是否有图片链接来创建数据。
java复制代码/** * @author: jiangjs * @description: 表格 * @date: 2022/11/24 17:19 **/ @EqualsAndHashCode(callSuper = true) @Data @Accessors(chain = true) public class TableSeriesRenderData extends LabelData { /** * 表头 */ private TextRenderData[] header; /** * 表内容 */ private Listcontents; }
将表头与表格内容进行分开赋值,使其更加清晰。
java复制代码/** * @author: jiangjs * @description: * @date: 2022/11/24 17:20 **/ @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()); ListcontentData = new ArrayList<>(); tableData.getContents().forEach(con ->{ contentData.add(RowRenderData.build(con)); }); return new MiniTableRenderData(header,contentData); } }
java复制代码/** * @author: jiangjs * @description: 列表 * @date: 2023/6/2 15:46 **/ @EqualsAndHashCode(callSuper = true) @Data @Accessors(chain = true) public class ListRenderData extends LabelData{ /** * 列表数据集 */ private Listlist; /** * 列表样式,支持罗马字符、有序无序等,默认为点 */ private Pair pair = NumbericRenderData.FMT_BULLET; }
java复制代码/** * @author: jiangjs * @description: * @date: 2023/6/2 15:42 **/ @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()); } }
java复制代码/** * @author: jiangjs * @description: 图表 * @date: 2022/11/24 11:38 **/ @EqualsAndHashCode(callSuper = true) @Data @Accessors(chain = true) public class ChartSeriesRenderData extends LabelData { /** * 横轴数据 */ private String[] categories; /** * 图表名称 */ private String title; /** * 图表类型 组合 */ private CharCombinationType charType = CharCombinationType.MULTI; /** * 系列对应数据 */ private ListsenderData; @Data public static class RenderData{ /** * 系列名称 */ private String renderTitle; /** * 系列对应的数据 */ private Number[] data; /** * 该系列对应生成的图表类型 */ private SeriesRenderData.ComboType comboType = null; } }
CharCombinationType:表示图表中系列的类型,只有单系列或多系列。
java复制代码/** * @author: jiangjs * @description: 图表系列类型 * @date: 2023/6/2 16:51 **/ public enum CharCombinationType { /** * 多组合 */ MULTI("Multi"), /** * 单图形 */ Single("Single"); private final String type; CharCombinationType(String type){ this.type = type; } public String getType(){ return type; } }
java复制代码/** * @author: jiangjs * @description: 图表类型 * @date: 2022/11/24 14:32 **/ @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()); ListrenderDataList = renderData.getSenderData(); List 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; } } }
代码中会根据系列类型的不同创建数据类型进行返回。
上述就是我对常用标签生成数据的封装,既然已经封装完成,那我们来进行下测试。
根据介绍的常用标签,我们在word中创建文件模板,如图:
我们将模板放置在项目的resources目录下。如图:
根据封装后的标签数据生成类来进行数据封装。
java复制代码 public void generateCharts() { File templateFile = null; try { templateFile = new ClassPathResource(TEMPLATE_PATH).getFile(); } catch (IOException e) { e.printStackTrace(); } Listgenerates = 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 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 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 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 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 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 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 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); }
从生成的word中我们看到,数据已经被替换,在word中生成。
上述就是我使用poi-tl生成word报表,也对其进行了封装,便于我们使用。
工欲善其事,必先利其器,有时候封装还是有必要的,希望我的封装对大家有所启发。
源码地址:github.com/lovejiashn/…