poi实现excel文件导入导出(基本数据导出、含格式导出、含批注导出、含图片图表导出)——springboot

这篇具有很好参考价值的文章主要介绍了poi实现excel文件导入导出(基本数据导出、含格式导出、含批注导出、含图片图表导出)——springboot。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本文主要是介绍springboot + poi实现基本的excel文件导入导出,包含数据导出导入时数据的其他需求校验,导出含有批注信息、导出含有图片信息、导出含有图表信息等的介绍等等,主要是一个demo尽可能简单明了的来介绍相关功能即可。有什么问题可以在留言哦!并在文章末尾附上demo源码下载!


一、前言

POI(Poor Obfuscation Implementation)是一个用于操作Microsoft Office格式文件的Java库。它提供了对Excel、Word和PowerPoint文件的读写功能,POI是一个功能强大的Java库,可以帮助开发人员在Java应用程序中处理Excel文件。无论是创建、编辑、读取还是导出数据,POI都提供了丰富的功能和灵活的接口。

单纯做数据的导出导入的话用easyExcel是比较方便,如果对数据导出有特殊要求的话,建议还是使用poi来按照需求进行设置,比如数据导入时数据校验,导入时数据的校验和单元格样式设置,导出含有批注信息、导出含有图片信息、导出含有图表信息等的介绍等等,这种情况还是poi的api用起比较舒服;注意如果不同版本的poi可能部分api是不兼容的,注意版本信息

poi导入导出excel,excel,java,spring boot

 实体类对应的excel表数据poi导入导出excel,excel,java,spring boot

 二、导入依赖和创建自定义注解以及实体类

1、导入依赖

注意版本信息,不同的poi版本信息是不一样的;

经常在使用poi相关功能的时候,会导入一些其他模块依赖来支撑;其中场景的依赖如下:

poi模块:提供了对Microsoft Office格式文件的基本操作,如读取、写入和修改Excel、Word和PowerPoint文件。
poi-scratchpad模块:提供了对一些较新的Microsoft Office格式文件的支持,如Excel 2007及以上版本的xlsx文件。
poi-ooxml模块:提供了对Microsoft Office Open XML格式文件的支持,如xlsx、docx和pptx文件。
poi-ooxml-schemas模块:提供了对Microsoft Office Open XML格式文件的底层支持,包含了所有的XML Schema定义。
poi-excelant模块:提供了对Excel宏的支持,可以执行和操作Excel宏。
这些模块之间存在依赖关系,具体如下:
poi-scratchpad依赖于poi模块,扩展了poi模块的功能,使其能够处理较新的Excel格式文件。
poi-ooxml依赖于poi模块,提供了对Microsoft Office Open XML格式文件的支持。
poi-ooxml-schemas依赖于poi-ooxml模块,提供了对Microsoft Office Open XML格式文件的底层支持。
poi-excelant依赖于poi模块,提供了对Excel宏的支持。
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.26</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.17</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.17</version>
        </dependency>
    </dependencies>

2、自定义注解

poi导入导出excel,excel,java,spring boot

 3、创建实体类

package com.jdh.poi.excel.pojo;

import com.jdh.poi.excel.anno.PoiExcel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @ClassName: Admin
 * @Author: jdh
 * @CreateTime: 2022-08-03
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Admin {

    @PoiExcel(cellIndex = 0, cellName = "编号")
    private Integer id;

    @PoiExcel(cellIndex = 1, cellName = "名称")
    private String name;

    @PoiExcel(cellIndex = 2, cellName = "区号")
    private String zone;

    @PoiExcel(cellIndex = 3, cellName = "能量值")
    private Double energy;

    @PoiExcel(cellIndex = 4, cellName = "创建时间")
    private Date createTime;

    @PoiExcel(cellIndex = 5, cellName = "备注")
    private String remark;

}

三、excel导出导入的功能基本实现

其中包含数据导入时数据校验,导入时数据的校验和单元格样式设置,导出含有批注信息、导出含有图片信息、导出含有图表信息等的介绍等等

1、获取excel的基本数据信息

  /**
     * 获取excel的基本数据信息,如sheet、行、列等
     *
     * @return
     * @throws Exception
     */
    @Test
    public void excelReadBasicInfo() throws Exception {
        FileInputStream inputStream = new FileInputStream(path + "admin_basic.xlsx");

        //下面就用到poi的api了
        Workbook workbook = WorkbookFactory.create(inputStream);
//        Workbook workbook = new XSSFWorkbook(inputStream);
        int sheetCount = workbook.getNumberOfSheets();
        System.out.println("Sheet数量:" + sheetCount);

        for (int i = 0; i < sheetCount; i++) {
            Sheet sheet = workbook.getSheetAt(i);

            // 获取行数
            int rowCount = sheet.getLastRowNum() + 1;

            //获取表头列数
            Row row0 = sheet.getRow(0);
            int firstCellNum = row0.getLastCellNum();

            // 获取表中最大列数
            int columnMaxCount = 0;
            for (int j = 0; j < rowCount; j++) {
                Row row = sheet.getRow(j);
                if (row != null) {
                    int currentColumnCount = row.getLastCellNum();
                    if (currentColumnCount > columnMaxCount) {
                        columnMaxCount = currentColumnCount;
                    }
                }
            }

            //注意:若读取行列数,如果单元格设置了单元格格式或者写入数据又给删掉,那么这种情况会被认为时有效单元格,在统计行或列的时候会被统计出来,但是肉眼看则时空行或列
            //在解析数据的时候也会去解析该单元格数据,会解析出一个空数据,所以在解析数据的时候就需要增加单元格数据类型校验
            System.out.println("Sheet名称:" + sheet.getSheetName() + ";行数:" + rowCount + ";表头列数:" + firstCellNum + ";最大列数:" + columnMaxCount);
        }

        System.out.println();

        workbook.close();
        inputStream.close();

    }

2、直接读取excel中的数据

 /**
     * 直接读取excel中的数据
     *
     * @return
     * @throws FileNotFoundException
     */
    @Test
    public void excelRead() throws FileNotFoundException {
        File file = new File(path + "admin_basic.xlsx");
        String fileType = file.getName().substring(file.getName().lastIndexOf(".") + 1);

        FileInputStream inputStream = null;

        Workbook wb = null;
        try {
            inputStream = new FileInputStream(file);

            //这里可以不用判断文件类型,直接使用WorkbookFactory来创建一个Workbook
            //Workbook wb = WorkbookFactory.create(inputStream);
            //判断是什么格式
            if (fileType.equals("xlsx")) {
                wb = new XSSFWorkbook(inputStream);
            } else if (fileType.equals("xls")) {
                wb = new HSSFWorkbook(inputStream);
            }

            Map<Integer, List<List<Object>>> allExcelData = new HashMap<>();

            //获取当前excel文档的sheet页数
            assert wb != null;
            int sheetCount = wb.getNumberOfSheets();//sheet页数
            //读取sheet的数据
            for (int i = 0; i < sheetCount; i++) {
                List<List<Object>> allSheetData = new ArrayList<>();//存储当前sheet的数据
                //获取每一个sheet
                Sheet sheet = wb.getSheetAt(i);
                //每一个sheet的表名
                String sheetName = sheet.getSheetName();
                //获取每一页sheet的行数(包括标题行);若有的行单元格为设置了格式等操作但无数据,也会被计算的
                int rows = sheet.getPhysicalNumberOfRows();
                //读取每个sheet的行数据
                for (int x = 0; x < rows; x++) {
                    ArrayList<Object> rowData = new ArrayList<>();//存储当前行的数据
                    //获取每一行数据
                    Row row = sheet.getRow(x);
                    //获取最右边有数据列的列数,若有的列单元格为设置了格式等操作但无数据,也会被计算的
                    int cellNum = row.getLastCellNum();
                    //获取对应有数据的有效列数,若有的列单元格为设置了格式等操作但无数据,也会被计算的
                    int realCellNum = row.getPhysicalNumberOfCells();

                    //读取每一行的每一个列的单元格数据
                    for (int y = 0; y < cellNum; y++) {
                        Cell cell = row.getCell(y);
                        Object value = getPoiExcelCellValueByType(cell);
                        rowData.add(value);//添加当前单元格数据
                    }
                    System.out.println("表:" + sheetName + ";行:" + x + ";最大列数:" + cellNum + ";有效列数:" + realCellNum);
                    allSheetData.add(rowData);//添加当前行数据
                }
                allExcelData.put(i, allSheetData);//添加当前sheet数据
            }
            System.out.println();
            //输出
            for (int i = 0; i < sheetCount; i++) {
                List<List<Object>> sheetData = allExcelData.get(i);
                sheetData.forEach(r -> {
                    r.forEach(c -> System.out.print(c + "   "));
                    System.out.println();
                });
                System.out.println("=========");
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                //关流操作(别忘了)
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

注意:读取excel数据的时候,需要对当前单元格数据做数据校验和转换

/**
     * 解析excel单元格数据类型和数据
     * CellType._NONE	未知类型,仅限内部使用
     * CellType.NUMERIC	数值型-整数、小数、日期	0
     * CellType.STRING	字符串型	1
     * CellType.FORMULA	公式型	2
     * CellType.BLANK	空单元格-没值,但是有单元格样式	3
     * CellType.BOOLEAN	布尔型	4
     * CellType.ERROR	错误单元格	5
     *
     * @param cell
     * @return
     */
    public static Object getPoiExcelCellValueByType(Cell cell) {

        if (Objects.isNull(cell)) return null;

        Object val;
        CellType cellTypeEnum = cell.getCellTypeEnum();

        if (cellTypeEnum == CellType.BLANK) {
            val = "";
        } else if (cellTypeEnum == CellType.NUMERIC) {
            if (HSSFDateUtil.isCellDateFormatted(cell)) { //date日期格式
                val = HSSFDateUtil.getJavaDate(cell.getNumericCellValue());
            } else { // 纯数字 整数/浮点数
                //整数
                if (Math.floor(cell.getNumericCellValue()) == cell.getNumericCellValue()) {
                    val = new DecimalFormat("0").format(cell.getNumericCellValue());
                } else {//小数
                    val = new DecimalFormat("0.00").format(cell.getNumericCellValue());
                }
            }
        } else if (cellTypeEnum == CellType.STRING) {
            val = cell.getStringCellValue();
        } else if (cellTypeEnum == CellType.FORMULA) {
            val = cell.getCellFormula();
        } else if (cellTypeEnum == CellType.BOOLEAN) {
            val = cell.getBooleanCellValue();
        } else if (cellTypeEnum == CellType.ERROR) {
            val = cell.getErrorCellValue();
        } else if (cellTypeEnum == CellType._NONE) {
            val = null;
        } else {
            val = null;
        }

        return val;
    }

3、将已有的数据导出为excel文件,并设置相关单元格格式

  /**
     * 将已有的数据导出为excel文件,并设置相关单元格格式
     *
     * @return
     * @throws FileNotFoundException
     */
    @Test
    public void excelWrite() throws FileNotFoundException {
        String fileName = "admin数据导出_PoiExcel.xlsx";
        String sheetName = "Sheet";

        //获取一个文件输出流
        FileOutputStream outputStream = new FileOutputStream(path + fileName);

        //构建workbook对象
        XSSFWorkbook workbook = new XSSFWorkbook();

        XSSFCellStyle style = workbook.createCellStyle();//单元格样式

//        style.setFillForegroundColor((short) 10);// 设置背景色
//        style.setFillBackgroundColor((short) 10);//设置背景填充色
//        style.setFillPattern(FillPatternType.BRICKS);//设置背景色规则

        style.setBorderBottom(BorderStyle.DASHED); //下边框
        style.setBorderLeft(BorderStyle.DASH_DOT_DOT);//左边框

        //字体样式是需要设置在cell列单元格上才生效
        Font font = workbook.createFont();//字体样式
        font.setBold(false);//字体是否加粗
        font.setFontName("宋体");//字体
        font.setFontHeightInPoints((short) 12);//设置字体大小
        font.setFontHeight((short) 300);//字体高度
        font.setColor((short) 20);//字体颜色
//        font.setColor(IndexedColors.BLACK.getIndex());
        style.setFont(font);

        //文本对齐方式,针对单元格有效
        style.setVerticalAlignment(VerticalAlignment.CENTER);// 垂直
        style.setAlignment(HorizontalAlignment.CENTER);// 水平居中

        //创建sheet表
        XSSFSheet sheet = workbook.createSheet("sheet1");

        //下面创建表头数据,也可以自定义一个注解,通过反射来获取类字段上的注解数据,然后通过注解映射来完成字段生成表头数据
        Row headRow = sheet.createRow(0);//创建一行数据

        //设置行高
//        headRow.setHeight((short) 600);//以像素的1/20
        headRow.setHeightInPoints(50);//这个是以像素点为基础

        //将表头单元格数据样式设置为上面的样式
        headRow.setRowStyle(style);

        //合并单元格
        CellRangeAddress cellRangeAddress = new CellRangeAddress(0, 0, 8, 11);
        sheet.addMergedRegion(cellRangeAddress);

        //设置列宽,但是这种直接设置会把其他列的宽度挤掉,因此在创建列的时候设置最好
        sheet.setColumnWidth(1, 200);

        Cell cell0 = headRow.createCell(0);//在第一行上创建一单元格
        cell0.setCellValue("编号");//赋值
        cell0.setCellType(CellType.STRING);//设置该单元格格式
        cell0.setCellStyle(style);

        Cell cell1 = headRow.createCell(1);
        cell1.setCellValue("名称");
        cell1.setCellType(CellType.STRING);

        Cell cell2 = headRow.createCell(2);
        cell2.setCellValue("区号");
        cell2.setCellType(CellType.STRING);

        Cell cell3 = headRow.createCell(3);
        cell3.setCellValue("能量值");
        cell3.setCellType(CellType.STRING);

        Cell cell4 = headRow.createCell(4);
        cell4.setCellValue("时间");
        cell4.setCellType(CellType.STRING);

        Cell cell5 = headRow.createCell(5);
        cell5.setCellValue("备注");
        cell5.setCellType(CellType.STRING);

        //用于解决时间转换为double格式设置
        CellStyle dateCellStyle = workbook.createCellStyle();
        CreationHelper creationHelper = workbook.getCreationHelper();
        dateCellStyle.setDataFormat(creationHelper.createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss"));
        for (int i = 0; i < data.size(); i++) {
            Row row = sheet.createRow(i + 1);//创建正文行数据
            //设置单元格格式;这一步可以通过一个类字段类型和excel类型一个map映射;这样可以通过反射需要导出的类上字段的类型来字段确认excel单元格类型
            row.createCell(0).setCellType(CellType.NUMERIC);
            row.createCell(1).setCellType(CellType.STRING);
            row.createCell(2).setCellType(CellType.STRING);
            row.createCell(3).setCellType(CellType.NUMERIC);
            row.createCell(4).setCellType(CellType.NUMERIC);
            row.createCell(5).setCellType(CellType.STRING);
            //设置数据
            row.getCell(0).setCellValue(data.get(i).getId());
            row.getCell(1).setCellValue(data.get(i).getName());
            row.getCell(2).setCellValue(data.get(i).getZone());
            row.getCell(3).setCellValue(data.get(i).getEnergy());
            row.getCell(4).setCellStyle(dateCellStyle);
            row.getCell(4).setCellValue(data.get(i).getCreateTime());
            row.getCell(5).setCellValue(data.get(i).getRemark());
        }

        try {
            workbook.write(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                workbook.close();
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

4、导出的excel添加批注信息

    /**
     * 导出的excel添加批注信息
     * <p>
     * XSSFClientAnchor是Apache POI库中用于在Excel中插入图像的类。它用于指定图像的位置和大小
     * XSSFClientAnchor的构造函数具有以下参数设置和意义:
     * 参数dx1: 图像或形状左上角相对于单元格左上角的X偏移量,以EMU(英制单位)为单位
     * 参数dy1: 图像或形状左上角相对于单元格左上角的Y偏移量,以EMU为单位。
     * 参数dx2: 图像或形状右下角相对于单元格左上角的X偏移量,以EMU为单位。
     * 参数dy2: 图像或形状右下角相对于单元格左上角的Y偏移量,以EMU为单位。
     * 参数col1: 单元格的起始列索引。表示图像所在单元格的起始列索引
     * 参数row1: 单元格的起始行索引。表示图像所在单元格的起始行索引
     * 参数col2: 单元格的结束列索引。表示图像所在单元格的结束列索引
     * 参数row2: 单元格的结束行索引。表示图像所在单元格的结束行索引。
     * 注意: dx1、dy1、dx2、dy2的范围为0-1024,col1、col2、row1、row2的范围为0-65535.
     * 使用XSSFClientAnchor可以将图像精确地定位到单元格上,并指定图像在单元格内的大小
     *
     * @throws FileNotFoundException
     */
    @Test
    public void excelPostWrite() throws FileNotFoundException {
        String fileName = "admin数据批注模板导出_PoiExcel.xlsx";
        String sheetName = "Sheet";

        //获取一个文件输出流
        FileOutputStream outputStream = new FileOutputStream(path + fileName);

        //构建workbook对象
        XSSFWorkbook workbook = new XSSFWorkbook();
        XSSFCellStyle cellHeadStyle = workbook.createCellStyle();//单元格样式
        cellHeadStyle.setVerticalAlignment(VerticalAlignment.CENTER);// 垂直
        cellHeadStyle.setAlignment(HorizontalAlignment.CENTER);// 水平
        Font font = workbook.createFont();//字体样式
        font.setBold(true);//字体是否加粗
        font.setFontName("宋体");//字体
        font.setFontHeightInPoints((short) 10);//设置字体大小
        font.setFontHeight((short) 250);//字体高度
        font.setColor((short) 20);//字体颜色
        cellHeadStyle.setFont(font);

        //创建sheet表
        XSSFSheet sheet = workbook.createSheet(sheetName);
        //下面创建表头数据,也可以自定义一个注解,通过反射来获取类字段上的注解数据,然后通过注解映射来完成字段生成表头数据
        Row headRow = sheet.createRow(0);//创建一行数据

        CreationHelper creationHelper = workbook.getCreationHelper();//用于创建各种对象,如ClientAnchor和Comment。

        Drawing<?> drawing = sheet.createDrawingPatriarch();//用于在工作表上创建绘图对象

        Cell cell0 = headRow.createCell(0);//在第一行上创建一单元格
        cell0.setCellValue("编号");//赋值
        cell0.setCellType(CellType.STRING);//设置该单元格格式
        cell0.setCellStyle(cellHeadStyle);
        ClientAnchor anchor0 = creationHelper.createClientAnchor();//用于指定批注的位置
        Comment comment0 = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, 0, 0, 3, 3));//用于创建批注对象
        comment0.setString(creationHelper.createRichTextString("这是数据编号,根据排列顺序而定,由系统生成"));//用于创建富文本字符串,可以设置批注的内容
        cell0.setCellComment(comment0);//将批注与单元格关联

        Cell cell1 = headRow.createCell(1);
        cell1.setCellValue("名称");
        cell1.setCellType(CellType.STRING);
        cell1.setCellStyle(cellHeadStyle);
        Comment comment1 = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, 1, 0, 4, 3));//用于创建批注对象
        comment1.setString(creationHelper.createRichTextString("这是数据对象名称,根据创建数据对象自定义,一经生成无法修改"));
        cell1.setCellComment(comment1);

        Cell cell2 = headRow.createCell(2);
        cell2.setCellValue("区号");
        cell2.setCellType(CellType.STRING);
        cell2.setCellStyle(cellHeadStyle);
        Comment comment2 = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, 2, 0, 5, 3));//用于创建批注对象
        comment2.setString(creationHelper.createRichTextString("这是数据对象所在区号,生成数据对象指定选中的区号,一经生成无法修改"));
        cell2.setCellComment(comment2);

        Cell cell3 = headRow.createCell(3);
        cell3.setCellValue("能量值");
        cell3.setCellType(CellType.STRING);
        cell3.setCellStyle(cellHeadStyle);
        Comment comment3 = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, 3, 0, 6, 3));//用于创建批注对象
        comment3.setString(creationHelper.createRichTextString("这是数据对象所拥有的能量值,由数据对象使用过程中累积的"));
        cell3.setCellComment(comment3);

        Cell cell4 = headRow.createCell(4);
        cell4.setCellValue("创建时间");
        cell4.setCellType(CellType.STRING);
        cell4.setCellStyle(cellHeadStyle);
        Comment comment4 = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, 4, 0, 7, 3));//用于创建批注对象
        comment4.setString(creationHelper.createRichTextString("这是数据对象创建的时间,一经生成无法修改"));
        cell4.setCellComment(comment4);

        Cell cell5 = headRow.createCell(5);
        cell5.setCellValue("备注");
        cell5.setCellType(CellType.STRING);
        cell5.setCellStyle(cellHeadStyle);
        Comment comment5 = drawing.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, 5, 0, 8, 3));//用于创建批注对象
        comment5.setString(creationHelper.createRichTextString("这是数据对象的备注"));
        cell5.setCellComment(comment5);

        //用于解决时间转换为double格式设置
        CellStyle dateCellStyle = workbook.createCellStyle();
        dateCellStyle.setDataFormat(creationHelper.createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss"));
        for (int i = 0; i < data.size(); i++) {
            Row row = sheet.createRow(i + 1);//创建正文行数据
            //设置单元格格式;这一步可以通过一个类字段类型和excel类型一个map映射;这样可以通过反射需要导出的类上字段的类型来字段确认excel单元格类型
            row.createCell(0).setCellType(CellType.NUMERIC);
            row.createCell(1).setCellType(CellType.STRING);
            row.createCell(2).setCellType(CellType.STRING);
            row.createCell(3).setCellType(CellType.NUMERIC);
            row.createCell(4).setCellType(CellType.NUMERIC);
            row.createCell(5).setCellType(CellType.STRING);
            //设置数据
            row.getCell(0).setCellValue(data.get(i).getId());
            row.getCell(1).setCellValue(data.get(i).getName());
            row.getCell(2).setCellValue(data.get(i).getZone());
            row.getCell(3).setCellValue(data.get(i).getEnergy());
            row.getCell(4).setCellStyle(dateCellStyle);
            row.getCell(4).setCellValue(data.get(i).getCreateTime());
            row.getCell(5).setCellValue(data.get(i).getRemark());
        }

        try {
            workbook.write(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                workbook.close();
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

5、导出excel文档,其中插入图片以及图表等数据

在导出图表的时候,总是会遇到设置其他类型图表类型,但导出来却没有变化,如果有大神知道,可以下放留言告诉我一下哦!

  /**
     * 导出excel文档,其中插入图片以及图表等数据
     *
     * @throws IOException
     */
    @Test
    public void excelImgChartWrite() throws IOException {
        String fileName = "数据图片图表模板导出_PoiExcel.xlsx";
        String sheetName = "Sheet";

        //获取一个文件输出流
        FileOutputStream outputStream = new FileOutputStream(path + fileName);

        // 创建工作簿
        Workbook workbook = new XSSFWorkbook();

        // 创建工作表
        Sheet sheet = workbook.createSheet(sheetName);

        for (int i = 0; i < 12; i++) {
            Row row = sheet.createRow(i);//创建正文行数据
            //设置数据
            row.createCell(0).setCellValue("第" + (i + 1) + "月");
            row.createCell(1).setCellValue(Math.random() * 100 + i);
            row.createCell(2).setCellValue(Math.random() * 100 + i);
            row.createCell(3).setCellValue(Math.random() * 100 + i);
            row.createCell(4).setCellValue(Math.random() * 100 + i);
            row.createCell(5).setCellValue(Math.random() * 100 + i);
        }

        // 读取图片文件
//        InputStream inputStream = getClass().getResourceAsStream("/image.jpeg");
        InputStream inputStream = new FileInputStream(path + "image.jpeg");
        byte[] imageBytes = IOUtils.toByteArray(inputStream);

        Drawing<?> drawing = sheet.createDrawingPatriarch();//用于在工作表上创建绘图对象

        // 将图片插入单元格
        int pictureIdx = sheet.getWorkbook().addPicture(imageBytes, Workbook.PICTURE_TYPE_JPEG);
        CreationHelper helper = sheet.getWorkbook().getCreationHelper();
        ClientAnchor anchor = helper.createClientAnchor();//用于指定图片的位置
        anchor.setCol1(9);//指定其实位置
        anchor.setRow1(0);
        anchor.setCol2(15);//指定结束位置
        anchor.setRow2(12);
        //这种写法和上面的一样
//        ClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, 9, 9, 15, 20);
        Picture picture = drawing.createPicture(anchor, pictureIdx);//用于创建图片对象
//        picture.resize();//将图像重置为嵌入图像的尺寸,如果重设置大小的话,那么上面设置图片位置只需要给的起始位置即可,图片会更具自身大小存放再excel中

        // 创建图表,并设置图表所在excel中区域位置
        ClientAnchor anchor1 = drawing.createAnchor(0, 0, 0, 0, 0, 15, 10, 35);
        XSSFChart chart = (XSSFChart) drawing.createChart(anchor1);//此版本需要转换为XSSFChart才能设置图表标题
        chart.setTitleText("模拟图表数据");//设置图表标题

        // 设置图表类型为折线图
//        chart.getCTChart().addNewPlotArea().addNewLineChart();
        // 设置图表类型和数据源
        ChartLegend legend = chart.getOrCreateLegend();
        legend.setPosition(LegendPosition.TOP_RIGHT);//设置统计数据标识所有图表位置


        //采用的数据来源于哪些表格,主要实现图表xy轴方向的坐标
        ChartDataSource<Number> xs = DataSources.fromNumericCellRange(sheet, new CellRangeAddress(0, 11, 0, 0));
        //下面分别是需要统计的数据,例如ys1是统计0-11行第1列的数据
        ChartDataSource<Number> ys1 = DataSources.fromNumericCellRange(sheet, new CellRangeAddress(0, 11, 1, 1));
        ChartDataSource<Number> ys2 = DataSources.fromNumericCellRange(sheet, new CellRangeAddress(0, 11, 2, 2));
        ChartDataSource<Number> ys3 = DataSources.fromNumericCellRange(sheet, new CellRangeAddress(0, 11, 3, 3));

        //将统计的数据添加到图表数据集中
//        LineChartData data = chart.getChartDataFactory().createLineChartData();
//        data.addSeries(xs, ys1);
//        data.addSeries(xs, ys2);
//        data.addSeries(xs, ys3);
        ScatterChartData data = chart.getChartDataFactory().createScatterChartData();
        data.addSerie(xs, ys1);
        data.addSerie(xs, ys2);
        data.addSerie(xs, ys3);


        ChartAxis bottomAxis = chart.getChartAxisFactory().createCategoryAxis(AxisPosition.BOTTOM);
        ValueAxis leftAxis = chart.getChartAxisFactory().createValueAxis(AxisPosition.LEFT);
        leftAxis.setCrosses(AxisCrosses.AUTO_ZERO);

        // 将图表插入单元格
        chart.plot(data, bottomAxis, leftAxis);

        workbook.write(outputStream);

        workbook.close();
        outputStream.close();
    }

四、对读取excel数据方法封装

1、封装读取的相关功能

结合自定义注解来实现读取excel数据的封装,其实思路和easyExcel的相关注解类似

package com.jdh.poi.excel.utils;

import com.jdh.poi.excel.anno.PoiExcel;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFDateUtil;
import org.apache.poi.ss.formula.functions.T;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.text.DecimalFormat;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @ClassName: PoiExcelReadOperation
 * @Author: jdh
 * @CreateTime: 2022-08-09
 * @Description: 指定类型读取excel数据封装
 */
@Slf4j
public class PoiExcelReadOperation {

    /**
     * 指定实体类读取数据
     * @param inputStream 文件输入流
     * @param clazz 实体类对象
     * @param checkHead 是否读取表头
     * @param <T>
     * @return 返回对应的数据map;k为第几张sheet,v为当前sheet中的数据
     * @throws Exception
     */
    public static <T> Map<Integer, List<T>> excelRead(InputStream inputStream, Class<T> clazz, Boolean checkHead) throws Exception {

        Workbook workbook = WorkbookFactory.create(inputStream);
        int sheetCount = workbook.getNumberOfSheets();

        Field[] fields = clazz.getDeclaredFields();
        List<Field> fieldList = Arrays.stream(fields).filter(f -> Objects.nonNull(f.getAnnotation(PoiExcel.class)))
                .sorted(Comparator.comparing(el -> el.getAnnotation(PoiExcel.class).cellIndex()))
                .collect(Collectors.toList());
        fieldList.forEach(f -> f.setAccessible(true));

        if(fieldList.size() == 0) return null;

        Map<Integer, List<T>> excelData = new HashMap<>();
        //读取sheet的数据
        for (int i = 0; i < sheetCount; i++) {
            Sheet sheet = workbook.getSheetAt(i);//获取每一个sheet
            String sheetName = sheet.getSheetName();//每一个sheet的表名
            //获取每一页sheet的行数(包括标题行);若有的行单元格为设置了格式等操作但无数据,也会被计算的
            int rows = sheet.getPhysicalNumberOfRows();
            ArrayList<T> sheetData = new ArrayList<>();
            //循环行数据
            int index = checkHead ? 1 : 0;
            for (int x = index; x < rows; x++) {
                T t = clazz.newInstance();
                //获取每一行数据
                Row row = sheet.getRow(x);
                //获取最右边有数据列的列数,若有的列单元格为设置了格式等操作但无数据,也会被计算的
                int cellNum = row.getLastCellNum();
                //读取每一行的每一个列的单元格数据
                for (int y = 0; y < cellNum; y++) {
                    Cell cell = row.getCell(y);
                    //将对应单元格数据赋值给对应的实例字段
                    if (fieldList.size() != 0){
                        if (y <= fieldList.size()) {
                            fieldList.get(y).set(t, getPoiExcelCellValueByType(cell));
                        }
                    }else {
//                        //如果没有指定PoiExcel注解,则采用默认顺序进行赋值
//                        if (y <= fields.length) {
//                            //这里应该还需要判断字段数据类型
//                            fields[y].set(t, (Object) getPoiExcelCellValueByType(cell));
//                        }
                    }
                }
                sheetData.add(t);
            }
            excelData.put(i,sheetData);
        }
        workbook.close();
        return excelData;
    }

    /**
     * CellType._NONE	未知类型,仅限内部使用 -1
     * CellType.NUMERIC	数值型-整数、小数、日期	0
     * CellType.STRING	字符串型	1
     * CellType.FORMULA	公式型	2
     * CellType.BLANK	空单元格-没值,但是有单元格样式	3
     * CellType.BOOLEAN	布尔型	4
     * CellType.ERROR	错误单元格	5
     *
     * @param cell 当前单元格
     * @return
     */
    public static Object getPoiExcelCellValueByType(Cell cell) {

        if (Objects.isNull(cell)) {
            log.info("cell is null");
            return null;
        }

        Object val;
        CellType cellTypeEnum = cell.getCellTypeEnum();

        if (cellTypeEnum == CellType.BLANK) {
            val = "";
        } else if (cellTypeEnum == CellType.NUMERIC) {
            if (HSSFDateUtil.isCellDateFormatted(cell)) { //date日期格式
                val = HSSFDateUtil.getJavaDate(cell.getNumericCellValue());
            } else { // 纯数字 整数/浮点数
                //整数
                if (Math.floor(cell.getNumericCellValue()) == cell.getNumericCellValue()) {
                    val = Integer.parseInt(new DecimalFormat("0").format(cell.getNumericCellValue()));
                } else {//小数
                    val = Double.parseDouble(new DecimalFormat("0.00").format(cell.getNumericCellValue()));
                }
            }
        } else if (cellTypeEnum == CellType.STRING) {
            val = cell.getStringCellValue();
        } else if (cellTypeEnum == CellType.FORMULA) {
            val = cell.getCellFormula();
        } else if (cellTypeEnum == CellType.BOOLEAN) {
            val = cell.getBooleanCellValue();
        } else if (cellTypeEnum == CellType.ERROR) {
            val = cell.getErrorCellValue();
        } else if (cellTypeEnum == CellType._NONE) {
            val = null;
        } else {
            val = null;
        }

        return val;
    }
}

2、对读取数据封装进行测试

package com.jdh.poi.excel.utils;

import com.jdh.poi.excel.pojo.Admin;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class PoiExcelReadOperationTest {

    private static final String path = System.getProperty("user.dir") + "\\Doc\\";

    @Test
    void excelRead() {
        FileInputStream inputStream = null;
        try {
            inputStream = new FileInputStream(path + "admin_basic.xlsx");
            Map<Integer, List<Admin>> listMap = PoiExcelReadOperation.excelRead(inputStream, Admin.class, true);
            assert listMap != null;
            List<Admin> admins = listMap.get(0);
            admins.forEach(System.out::println);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                assert inputStream != null;
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

五、对数据导出进行封装

1、封装数据导出相关功能

package com.jdh.poi.excel.utils;

import com.jdh.poi.excel.anno.PoiExcel;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @ClassName: PoiExcelReadOperation
 * @Author: jdh
 * @CreateTime: 2022-08-03
 * @Description: poiExcel指定实体类导出数据或者模板
 */
public class PoiExcelWriteOperation {

    /**
     * 根据指定实体类,导出excel文档数据
     *
     * @param outputStream 文件输出流
     * @param dataMap      需要导出的数据,key为sheet名称,v为需要导出的数据,若传null或者v为null则导含表头的模板
     * @param clazz        需要指定导出的实体类对象
     * @throws Exception
     */
    public static <T> void excelWrite(OutputStream outputStream, LinkedHashMap<String, List<T>> dataMap, Class<T> clazz) throws Exception {
        //构建workbook对象
        XSSFWorkbook workbook = new XSSFWorkbook();

        XSSFCellStyle style = workbook.createCellStyle();//单元格样式
        style.setVerticalAlignment(VerticalAlignment.CENTER);// 垂直
        style.setAlignment(HorizontalAlignment.CENTER);// 水平

        XSSFCellStyle cellHeadStyle = workbook.createCellStyle();//单元格样式
        cellHeadStyle.setVerticalAlignment(VerticalAlignment.CENTER);// 垂直
        cellHeadStyle.setAlignment(HorizontalAlignment.CENTER);// 水平
        Font font = workbook.createFont();//字体样式
        font.setBold(true);//字体是否加粗
        font.setFontName("宋体");//字体
        font.setFontHeightInPoints((short) 10);//设置字体大小
        font.setFontHeight((short) 250);//字体高度
        font.setColor((short) 20);//字体颜色
        style.setFont(font);
        cellHeadStyle.setFont(font);

        Field[] fields = clazz.getDeclaredFields();
        List<Field> fieldList = Arrays.stream(fields).filter(f -> Objects.nonNull(f.getAnnotation(PoiExcel.class)))
                .sorted(Comparator.comparing(el -> el.getAnnotation(PoiExcel.class).cellIndex()))
                .collect(Collectors.toList());
        fieldList.forEach(f -> f.setAccessible(true));

        //若对应的实体类的字段属性未指定PoiExcel注解则不认为导出
        if (fieldList.size() == 0) return;

        if (dataMap.size() == 0) {
            dataMap.put("sheet", null);
        }

        //所有需要导出excel对应sheet的名称
        ArrayList<String> sheetNameList = new ArrayList<>(dataMap.keySet());

        //循环当前需要创建的sheet
        for (int i = 0; i < dataMap.size(); i++) {
            //创建sheet表
            XSSFSheet sheet = workbook.createSheet(sheetNameList.get(i));
            //需要导出的数据集合
            List<T> ts = dataMap.get(sheetNameList.get(i));
            //如果对应的数据集合为空,则为模板导出模式
            int rowCount = Objects.isNull(ts) ? 0 : ts.size();

            Row headRow = sheet.createRow(0);//创建一行数据
            //下面创建表头数据
            headRow.setHeightInPoints(35);//这个是以像素点为基础
            //添加表头数据
            for (int x = 0; x < fieldList.size(); x++) {
                Field field = fieldList.get(x);
                PoiExcel poiExcel = field.getAnnotation(PoiExcel.class);
                Cell cell = headRow.createCell(x);
                cell.setCellValue(poiExcel.cellName());
                cell.setCellType(CellType.STRING);
                cell.setCellStyle(cellHeadStyle);
            }

            //循环当前的行(不含表头)
            for (int r = 0; r < rowCount; r++) {
                Row row = sheet.createRow(r + 1);
                T t = ts.get(r);//某一行的数据
                //循环列对每一个单元格赋值
                for (int x = 0; x < fieldList.size(); x++) {
                    Field field = fieldList.get(x);
                    Cell cell = row.createCell(x);
                    //这里赋值的时候,最好做实体类字段与单元格类型的映射关系,并赋值指定类型
                    String value = field.get(t).toString();
                    cell.setCellType(CellType.STRING);
                    cell.setCellValue(value);
                }
            }
        }

        workbook.write(outputStream);
        workbook.close();

    }

}

2、对导出数据封装进行测试

package com.jdh.poi.excel.utils;

import com.jdh.poi.excel.pojo.Admin;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class PoiExcelWriteOperationTest {

    private static final String path = System.getProperty("user.dir") + "\\Doc\\";

    //这个数据是模拟从数据库查询的数据
    public static final List<Admin> data = new ArrayList<>(Arrays.asList(
            new Admin(1, "admin001", "一区", 8500.05, new Date(), "qq一区管理员"),
            new Admin(2, "admin002", "二区", 8700.05, new Date(), "qq二区管理员"),
            new Admin(3, "admin003", "三区", 8800.05, new Date(), "qq三区管理员"),
            new Admin(4, "admin004", "四区", 8900.05, new Date(), "qq四区管理员"),
            new Admin(5, "admin005", "五区", 9500.05, new Date(), "qq五区管理员"),
            new Admin(6, "admin006", "六区", 10000.05, new Date(), "qq六区管理员")
    ));

    @Test
    void excelWrite() {
        //获取一个文件输出流
        FileOutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(path + "admin模板数据导出_poiExcel.xlsx");
            LinkedHashMap<String, List<Admin>> dataMap = new LinkedHashMap<>();
            dataMap.put("sheet01",data);
            dataMap.put("sheet02",data);
            PoiExcelWriteOperation.excelWrite(outputStream,dataMap,Admin.class);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                assert outputStream != null;
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

六、结尾

源码gitee地址:文章来源地址https://www.toymoban.com/news/detail-715892.html

EasyExcel和POI版本对应关系:

  • EasyExcel 3.0.x: POI 3.17
  • EasyExcel 3.1.x: POI 4.0.1
  • EasyExcel 3.1.4: POI 4.1.2

poi常用操作说明如下:

Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程式对Microsoft Office格式档案读和写的功能。
POI为“Poor Obfuscation Implementation”的首字母缩写,意为“简洁版的模糊实现”。

一、常用文档类型说明
HSSF : 读写 Microsoft Excel XLS 格式文档
XSSF : 读写 Microsoft Excel OOXML XLSX 格式文档
SXSSF : 读写 Microsoft Excel OOXML XLSX 格式文档
HWPF : 读写 Microsoft Word DOC 格式文档
HSLF : 读写 Microsoft PowerPoint 格式文档
HDGF : 读 Microsoft Visio 格式文档
HPBF : 读 Microsoft Publisher 格式文档
HSMF : 读 Microsoft Outlook 格式文档

二、POI常用类说明
类名 => 说明:
HSSFWorkbook => Excel的文档对象
HSSFSheet => Excel的表单
HSSFRow => Excel的行
HSSFCell => Excel的格子单元
HSSFFont => Excel字体
HSSFDataFormat => 格子单元的日期格式
HSSFHeader => Excel文档Sheet的页眉
HSSFFooter => Excel文档Sheet的页脚
HSSFCellStyle => 格子单元样式
HSSFDateUtil => 日期
HSSFPrintSetup => 打印
HSSFErrorConstants => 错误信息表

三、常用单元格数据类型说明
CellType => 单元格类型 => 对应的数值:
CellType._NONE => 未知类型,仅限内部使用 => -1
CellType.NUMERIC => 数值型-整数、小数、日期 => 0
CellType.STRING => 字符串型 => 1
CellType.FORMULA => 公式型 => 2
CellType.BLANK => 空单元格-没值,但是有单元格样式 => 3
CellType.BOOLEAN => 布尔型 => 4
CellType.ERROR => 错误单元格 => 5

Tips:
日期数据对应的单元格类型是CellType.NUMERIC,默认以浮点型数显示,显示为日期格式需要设置单元格样式DataFormat,
字符型单元格内容也可以为富文本RichTextString,可以对文本多部分设置字体Font

到了这里,关于poi实现excel文件导入导出(基本数据导出、含格式导出、含批注导出、含图片图表导出)——springboot的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • hutool poi、apache poi实现导入导出以及解析excel

    一、前言 看了例子之后后续需要更加深入学习或者更多理解其他API的话,建议看官方文档。hutool项目是中国人维护的,有中文文档,阅读起来很方便。apache poi比较底层一点,可以更加自由去二次开发自己所需的功能。 hutool官方文档 hutool官方gitee apache poi官方文档 二、基于

    2024年02月09日
    浏览(39)
  • Java原生POI实现的Excel导入导出(简单易懂)

    首先是Controller入口方法 这个接口在postman上传参是下面这样的: 注意里面的参数名称要和接口上的一致,不然会拿不到值 还有file那里key的类型要选file类型的,这样就可以在后面value里面选择文件 然后是Service方法 首先是Controller入口 strJson是用来接受其它参数的,一般导出的

    2024年02月11日
    浏览(35)
  • java实现excel的导入导出(带参数校验:非空校验、数据格式校验)

    本次封装引入阿里开源框架EasyExcel,EasyExcel是一个基于Java的简单、省内存的读写Excel的开源项目。在尽可能节约内存的情况下支持读写百M的Excel。 github地址:GitHub - alibaba/easyexcel: 快速、简洁、解决大文件内存溢出的java处理Excel工具 。 64M内存20秒读取75M(46W行25列)的Excel(3.0.2

    2024年02月01日
    浏览(53)
  • Java使用poi导出excel针对不同数据列配置设置不同单元格格式(适用于通用导出excel数据)

    公司大部分业务都是查询相关的业务, 所以建了一个项目专门做数据查询, 数据中转等抽象通用的业务, 有一天给我安排了一个功能, 做excel导出, 配置好查询sql和表头字段映射后即可导出excel, 无需修改代码 后来因为导出数据要求保留几位小数或者转换成百分比等设置单元格格

    2024年02月07日
    浏览(36)
  • poi实现Excel文件导出【SpringBoot篇】

    在系统中,数据库的excel文件导出是一项及为基础的功能。此篇文章将通过实例利用poi实现excel文件导出。 Jakarta POI 是apache的子项目,目标是处理ole2对象。它提供了一组操纵Windows文档的Java API 。目前比较成熟的是HSSF接口,处理MS Excel(97-2002)对象。它不象我们仅仅是用csv生

    2024年02月05日
    浏览(29)
  • java poi导入Excel、导出excel

    java poi导入Excel、导出excel ReadPatientExcelUtil PoiUtils FileUtils

    2024年02月15日
    浏览(27)
  • vue 实现前端处理 “数组数据“ 转excel格式文件,并导出excel表格

    一、安装插件 XLSX 二、页面引入        希望我的愚见能够帮助你哦~,若有不足之处,还望指出,你们有更好的解决方法,欢迎大家在评论区下方留言支持,大家一起相互学习参考呀~

    2024年01月18日
    浏览(40)
  • poi技术实现数据的导入与导出

    前言: POI是Apache软件基金会的开放源码函式库,POI提供API给Java程序对Microsoft Office格式档案读和写的功能。 Microsoft Office:是由微软公司开发的一套 办公软件套装 。常用组件有Word、Excel、PowerPoint等。 POI也用于操作Excel(电子表格) 一个excel文件就是一个工作簿Workbook 一个工

    2024年02月05日
    浏览(29)
  • 一文搞定POI,再也不怕excel导入导出了

    在Java日常开发过程中,实现Excel文件的导入导出功能是一项常见的需求。 通过使用相关的Java库,如Apache POI、EasyPoi或EasyExcel,可以轻松地实现Excel文件的读写操作。 而这篇文章将介绍如何在Java中使用Apache POI、EasyPoi 和EasyExcel库来进行Excel文件的导入和导出操作,帮助您快速掌

    2024年03月11日
    浏览(34)
  • JAVA:使用POI SXSSFWorkbook方式导出Excel大数据文件

    Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java对Microsoft Office格式档案读和写的功能。POI组件可以提供Java操作Microsoft Office的API,导出格式为Office 2003时POI调用的HSSF包,导出格式为Office 2007时,调用XSSF包,而SXSSF包是POI3.8版本之上对XSSF的一个扩展,用

    2024年02月11日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包