SpringBoot + POI-TL 操作 Word,快速生成报表,短小精悍!

戳上方蓝字“Java知音”关注我

前段时间做了一个需求:需要快速生成一份数据报告,里面包含了文字、图片和数据报表,同时生成的图形数据也可以随意修改。之前想着使用Apache POI来进行实现,在翻阅一些资料后,发现poi-tl更适合我们的业务,也更容易上手,于是对其进行了研究、也对其中的一些标签进行了封装,形成一个快速实现的工具类。

废话不多说,直接开撸。

1、Poi-tl简介

poi-tl是一个免费开源的Java类库,是基于Apache POI的模板引擎,纯Java组件,跨平台,代码短小精悍,通过插件机制使其具有高度扩展性。

因此在使用的时候需要实现设置好模板,就像Freemarker一样,但是比其简单,也易操作。

2、常用标签

2.1 文本

格式:{{var}}

数据模型:

例如:

put("name""Sayi");
put("author"new TextRenderData("000000""Sayi"));
// 超链接
put("link",
  new HyperLinkTextRenderData("website""http://deepoove.com"));

2.2 图片

格式:以@开始,{{@var}}

数据模型:

例如:

// 本地图片
put("local"new PictureRenderData(80100"./sayi.png"));

// 图片流
put("localbyte"new PictureRenderData(80100".png"new FileInputStream("./logo.png")));

// 网络图片(注意网络耗时对系统可能的性能影响)
put("urlpicture"new PictureRenderData(5050".png", BytePictureUtils.getUrlBufferedImage("http://deepoove.com/images/icecream.png")));

2.3 表格

poi-tl默认实现了N行N列的样式(如下图),同时提供了当数据为空时,展示一行空数据的文案。

格式:以#开头,{{#var}}

数据模型:

2.4 列表

格式:以*开头,{{*var}}

数据模型:

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 singleSeriesRenderData = new ChartSingleSeriesRenderData();
singleSeriesRenderData.setCategories(new String[] { "俄罗斯""加拿大""美国""中国" }");
singleSeriesRenderData.setChartTitle("
测试");
pie.setSeriesData(new SeriesRenderData("
countries", new Integer[] { 17098242, 9984670, 9826675, 9596961 }));

2.6 多系列图表

在报表应用中,很多时候使用的是多系列组合,例如:柱状图与折线图组合等。

格式:与单系列一致。

数据模型:

例如:

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.06.0 }));
seriesRenderData.add(new SeriesRenderData("speakers"new Double[] { 223.0119.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();
            }
        }
    }
}
  • tempFilePath:模板文件的地址。
  • 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[]{17098242998467098266759596961}).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[]{553523766065.170.275.380.485.590.695.726,
                766065.170.275.380.495.726766065.170.275.395.726766065.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

来源|juejin.cn/post/7241171237603147831


后端专属技术群

构建高质量的技术交流社群,欢迎从事编程开发、技术招聘HR进群,也欢迎大家分享自己公司的内推信息,相互帮助,一起进步!

文明发言,以交流技术职位内推行业探讨为主

广告人士勿入,切勿轻信私聊,防止被骗

加我好友,拉你进群

相关推荐

  • 相比于 Node.js,Deno 和 Bun 到底能带来什么?
  • 如何减少网页卡顿
  • Vite 5.1 正式发布,性能大幅提升!
  • 如何画好一张架构图?
  • Spring Boot 实现跨域的 5 种方式,总有一种适合你,建议收藏!!
  • 工作十几年,看到这样的代码,内心五味杂陈...
  • 同事不拿年终奖就走了,我问他:年底了,为啥离职?他笑了笑:走了就少了钱, 拖下去少的可能是命,他的心情和身体健康更重要。
  • 11k star,一个强大的 Java 版爬虫框架,几行代码即可实现一个爬虫
  • 真正的无服务器 Kafka 解决方案,可最大限度地发挥云的优势
  • 一文搞定JVM相关的命令汇总,推荐收藏!
  • 张宏江:大模型技术发展的八点观察
  • 刚刚!奥特曼放出来了GPT-5的内容:能力提升幅度超乎想象
  • 【2023新书】贝叶斯优化实战,426页pdf
  • 图解Transformer工作原理
  • 首届东木大会小记
  • 每日 prompt:可爱壁纸
  • 朱啸虎:AI应快速赚钱|杨植麟:AI要改变世界|王小川:AI要创造价值
  • 英伟达发布全新Blackwell GPU计算平台|Stability AI推3D内容生成工具
  • [开源]基于 SpringBoot 架构体系构建的ERP库存管理系统,高效便捷
  • OPC-UA是这样在食品和饮料中应用的