目录

  • 前端
  • Apache POI
  • 引依赖
  • 导入
  • 导出
  • 工具类
  • EasyExcel
  • 引依赖
  • 读Excel
  • 指定列名
  • 多个Sheet
  • 使用提供的转换器或自定义格式转换器对读取到的数据进行格式转换
  • 行头即列名
  • 写Excel
  • 指定导出列
  • 复杂头写入
  • 日期、数字或自定义格式转换对导出数据进行处理
  • 自定义样式
  • 合并单元格
  • 动态列表头
  • 自定义拦截器,对单元格进行操作
  • 工具类


前端

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>上传与下载</title>
	</head>
	<body>
		<form action="http://10.3.121.7:8889/file/upload" method="post" enctype="multipart/form-data">
			<input type="file" name="file">
			<button type="submit">上传</button>
		</form>
		<!-- download 属性用于指示浏览器下载该文件,而不是直接在浏览器中打开。 -->
		<a href="http://10.3.121.7:8889/file/download" download>下载</a>
	</body>
</html>

在接收文件时可用的参数名与 HTML 表单中 input 标签的 name 属性相同。如果不指定 name 属性,将默认使用 file 作为参数名。

Apache POI

最原始的Java导入导出方法。

引依赖

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>4.1.2</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
</dependency>

导入

@PostMapping(value = "/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
    Result<Object> result = new Result<>();
    try {
        InputStream is = file.getInputStream();
        // 创建 Workbook 对象
        XSSFWorkbook workbook = new XSSFWorkbook(is);

        // 获取第一个 Sheet
        XSSFSheet sheet = workbook.getSheetAt(0);

        // 遍历数据表格
        for (org.apache.poi.ss.usermodel.Row item : sheet) {
            XSSFRow row = (XSSFRow) item;
            Iterator<Cell> cells = row.cellIterator();

            // 遍历行中的每个单元格
            while (cells.hasNext()) {
                XSSFCell cell = (XSSFCell) cells.next();
                //需要确保表格的数据都是数字,也有其他方法获取其他类型数据
                double value = cell.getNumericCellValue();
                System.out.print(value + " ");
            }
            System.out.println();
        }
        // 关闭输入流
        is.close();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
    result.setSuccess(true);
    return result;
}

不使用springboot时:

HttpServletRequest request
// 获取上传文件
Part filePart = request.getPart("file");
String fileName = filePart.getSubmittedFileName();
InputStream fileContent = filePart.getInputStream();

导出

public void download(HttpServletResponse response) {
    try {
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        //处理文件名乱码
        String fileName = URLEncoder.encode("test.xlsx", "UTF-8");
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
        // 创建 Workbook 对象,即工作簿Excel文件
        XSSFWorkbook workbook = new XSSFWorkbook();

        // 创建 Sheet 对象
        XSSFSheet sheet = workbook.createSheet("test");

        // 创建行对象和单元格对象
        // 第一行,即列表头
        XSSFRow row = sheet.createRow(0);
        XSSFCell cell1 = row.createCell(0);
        XSSFCell cell2 = row.createCell(1);
        XSSFCell cell3 = row.createCell(2);
        XSSFCell cell4 = row.createCell(3);


        // 设置单元格的值
        cell1.setCellValue("Hello");
        cell2.setCellValue("World");
        // 往第三个单元格塞入日期值,这需要进行格式处理
        XSSFCreationHelper helper = workbook.getCreationHelper();
        XSSFCellStyle style = workbook.createCellStyle();
        style.setDataFormat(helper.createDataFormat().getFormat("yyyy-MM-dd"));
        cell3.setCellValue(new Date());
        cell3.setCellStyle(style);
        // 第四个单元格塞入数字,保留两位小数
        XSSFCellStyle style1 = workbook.createCellStyle();
        style1.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00"));
        cell4.setCellStyle(style1);
        cell4.setCellValue(3.1415d);

        // 输出数据到文件
        ServletOutputStream outputStream = response.getOutputStream();
        workbook.write(outputStream);
        outputStream.close();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

工具类

import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;

@Component
public class ExcelUtils {

    /**
     * 将指定类型的对象列表导出为 Excel 文件
     *
     * @param list     对象列表
     * @param clazz    对象类型
     * @param response 获取输出流
     * @throws IOException IO 异常
     */
    public static <T> void toExcel(List<T> list, Class<T> clazz, HttpServletResponse response) throws IOException {
        // 创建 Excel 工作簿并写入数据
        Workbook workbook = new XSSFWorkbook();
        writeHeader(workbook, clazz);
        writeBody(workbook, clazz, list);

        // 将 Excel 工作簿写入输出流
        ServletOutputStream out = response.getOutputStream();
        workbook.write(out);
    }

    /**
     * 写入 Excel 表头
     *
     * @param workbook Excel 工作簿
     * @param clazz    对象类型
     */
    public static void writeHeader(Workbook workbook, Class<?> clazz) {
        Sheet sheet = workbook.createSheet();
        Row headerRow = sheet.createRow(0);
        List<Field> fields = getFields(clazz);
        for (int i = 0; i < fields.size(); i++) {
            Field field = fields.get(i);
            Cell cell = headerRow.createCell(i);
            cell.setCellValue(field.getName());
        }
    }

    /**
     * 写入 Excel 数据
     *
     * @param workbook Excel 工作簿
     * @param clazz    对象类型
     * @param list     对象列表
     */
    public static <T> void writeBody(Workbook workbook, Class<T> clazz, List<T> list) {
        Sheet sheet = workbook.getSheetAt(0);
        List<Field> fields = getFields(clazz);
        for (int i = 0; i < list.size(); i++) {
            Row row = sheet.createRow(i + 1);
            T obj = list.get(i);
            for (int j = 0; j < fields.size(); j++) {
                Field field = fields.get(j);
                field.setAccessible(true);
                try {
                    Object value = field.get(obj);
                    if (value != null) {
                        Cell cell = row.createCell(j);
                        cell.setCellValue(value.toString());
                    }
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 获取对象的所有字段,包括父类的字段
     *
     * @param clazz 对象类型
     * @return 对象的所有字段
     */
//    public static List<Field> getFields(Class<?> clazz) {
//        List<Field> fields = new ArrayList<>();
//        while (clazz != null) {
//            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
//            clazz = clazz.getSuperclass();
//        }
//        return fields;
//    }

    /**
     * 将输入流中的 Excel 文件解析为指定类型的对象列表
     *
     * @param inputStream 输入流
     * @param clazz       对象类型
     * @return 对象列表
     * @throws IOException IO 异常
     * @throws Exception   Excel 解析异常
     */
    public static <T> List<T> readExcel(InputStream inputStream, Class<T> clazz) throws IOException, Exception {
        // 读取 Excel 工作簿
        Workbook workbook = WorkbookFactory.create(inputStream);
        if (workbook.getNumberOfSheets() < 1) {
            throw new Exception("Excel 文件中没有工作表");
        }
        // 解析 Excel 工作表
        List<String> header = getHeader(workbook.getSheetAt(0).getRow(0));
        List<Field> fields = getFields(clazz);
        List<T> list = new ArrayList<>();
        for (int i = 1; i <= workbook.getSheetAt(0).getLastRowNum(); i++) {
            T obj = newInstance(clazz);
            for (int j = 0; j < header.size(); j++) {
                String fieldName = header.get(j);
                Field field = fields.stream()
                        .filter(f -> f.getName().equalsIgnoreCase(fieldName))
                        .findFirst()
                        .orElse(null);
                if (field != null) {
                    Class<?> fieldType = field.getType();
                    Object cellValue = getCellValue(workbook.getSheetAt(0).getRow(i).getCell(j), fieldType);
                    setFieldValue(obj, field, cellValue);
                }
            }
            list.add(obj);
        }
        return list;
    }

    // 获取 Excel 表头
    private static List<String> getHeader(Row row) {
        List<String> header = new ArrayList<>();
        for (Cell cell : row) {
            header.add(cell.getStringCellValue());
        }
        return header;
    }

    // 获取实体类的所有属性
    private static List<Field> getFields(Class<?> clazz) {
        List<Field> fields = new ArrayList<>();
        Class<?> superClazz = clazz.getSuperclass();
        if (superClazz != null) {
            fields.addAll(getFields(superClazz));
        }
        fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
        return fields;
    }

    // 根据类型获取单元格的值
    private static Object getCellValue(Cell cell, Class<?> targetType) {
        if (cell == null) {
            return null;
        } else if (targetType == String.class) {
            return cell.getStringCellValue();
        } else if (targetType == Integer.class) {
            return (int) cell.getNumericCellValue();
        } else if (targetType == Double.class) {
            return cell.getNumericCellValue();
        } else if (targetType == Boolean.class) {
            return cell.getBooleanCellValue();
        } else if (targetType == Date.class) {
            return cell.getDateCellValue();
        } else if (targetType == byte[].class) {
            // TODO: 处理二进制文件单元格的值
            return null;
        } else {
            return null;
        }
    }

    // 设置实体类对象的属性值
    private static void setFieldValue(Object obj, Field field, Object value) throws IllegalAccessException {
        field.setAccessible(true);
        field.set(obj, value);
    }

    // 创建指定类型的新实例
    private static <T> T newInstance(Class<T> clazz) throws Exception {
        try {
            return clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException ex) {
            throw new Exception("无法创建新的 " + clazz.getName() + " 实例", ex);
        }
    }
}

EasyExcel

EasyExcel 是一款基于阿里巴巴的 EasyPOI 开源项目封装的 Java Excel 读写库,它具有轻量、易用和高性能的特点。

引依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.2.10</version>
</dependency>

读Excel

读数据需要一个监听器

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
public class ExcelListener<T> extends AnalysisEventListener<T> {
    private final List<T> rows = new ArrayList<>();
    private final List<String> head = new ArrayList<>();

    @Override
    public void invoke(T object, AnalysisContext context) {
        rows.add(object);
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        log.info("read {} rows", rows.size());
    }

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
        head.addAll(headMap.values());
    }

    public List<T> getRows() {
        return rows;
    }

    public List<String> getHead() {
        return head;
    }

}
try {
    InputStream inputStream = file.getInputStream();
    ExcelListener<Object> listener = new ExcelListener<>();
    ExcelReaderBuilder read = EasyExcel.read(inputStream, listener);
    read.doReadAll();
    //数据
    System.out.println(listener.getRows());
    //列名
    System.out.println(listener.getHead());
} catch (IOException e) {
    throw new RuntimeException(e);
}

指定列名

import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;

import java.io.Serializable;

@Data
public class Task implements Serializable {

    private static final long serialVersionUID = 1L;

    @ExcelProperty(value = "*车架号")
    private String vin;

    @ExcelProperty(value = "*任务信息")
    private String taskInfo;
}

使用注解ExcelProperty

try {
    InputStream inputStream = file.getInputStream();
    ExcelListener<Task> listener = new ExcelListener<>();
    EasyExcel.read(inputStream, Task.class, listener).sheet().doRead();
    List<Task> rows = listener.getRows();
    System.out.println(rows);
} catch (IOException e) {
    throw new RuntimeException(e);
}
try {
    InputStream inputStream = file.getInputStream();
    ExcelListener<Task> listener = new ExcelListener<>();
    ExcelReader reader = EasyExcel.read(inputStream, Task.class, listener).build();
    ReadSheet sheet = EasyExcel.readSheet().build();
    reader.read(sheet);
    List<Task> rows = listener.getRows();
    System.out.println(rows);
} catch (IOException e) {
    throw new RuntimeException(e);
}

多个Sheet

try {
    InputStream inputStream = file.getInputStream();
    ExcelListener<Task> listener = new ExcelListener<>();
    ExcelReader reader = EasyExcel.read(inputStream, Task.class, listener).build();
    List<ReadSheet> readSheets = reader.excelExecutor().sheetList(); //文件里Sheet的信息
    /*
    [ReadSheet{sheetNo=0, sheetName='Sheet1'} com.alibaba.excel.read.metadata.ReadSheet@4aee277, 
    ReadSheet{sheetNo=1, sheetName='Sheet2'} com.alibaba.excel.read.metadata.ReadSheet@57915590, 
    ReadSheet{sheetNo=2, sheetName='Sheet3'} com.alibaba.excel.read.metadata.ReadSheet@6f73f2d9]
     */
    System.out.println(readSheets);
    System.out.println(readSheets.size()); //3
    ReadSheet sheet = EasyExcel.readSheet(0).build();
    ReadSheet sheet1 = EasyExcel.readSheet(1).build();
    reader.read(sheet, sheet1);
    List<Task> rows = listener.getRows();
    result.setResult(rows);
} catch (IOException e) {
    throw new RuntimeException(e);
}
try {
    InputStream inputStream = file.getInputStream();
    ExcelListener<Task> listener = new ExcelListener<>();
    EasyExcel.read(inputStream, Task.class, listener).doReadAll();
    result.setResult(listener.getRows());
} catch (IOException e) {
    throw new RuntimeException(e);
}

注:ExcelListener的doAfterAllAnalysed 方法会在每个sheet读取完毕后调用一次。然后所有sheet读完后都会往同一个ExcelListener里面写,即最后所有的数据都会汇总到一个ExcelListener对象里。

当在EasyExcel的read方法中确定了实体类时,如上面的Task实体类,那么就会将Sheet列名和实体类里的ExcelProperty注解的value值匹配上,如果Sheet里没有这两列或者两列下没有值但是其他列有值那么返回的数据依然没有值。

监听器invoke方法读取到的数据是默认从第一行开始的。对应的就是Sheet的第2行。也就是该方法不触碰列名那一行。

使用提供的转换器或自定义格式转换器对读取到的数据进行格式转换

行头即列名

当Excel没有列名或者有多行列名可以使用这个办法来说明。

EasyExcel.read(inputStream, listener).sheet().headRowNumber(0).doRead();

headRowNumber方法用于指定行头有几行参数有:

  • 0:表示没有行头,即第一行就是数据
  • 1:默认值就是1,表示第一行是行头,从第2行开始是数据
  • 2:表示行头有两行,从第3行开始是数据

如果想要读取行头,上面的监听器invokeHeadMap方法保存了行头的数据。

写Excel

使用ExcelProperty注解指定写的列名,ExcelIgnore注解让对象的某属性不写。

@GetMapping(value = "/download")
public void download(HttpServletResponse response) {
    try {
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("UTF-8");
        String fileName = URLEncoder.encode("测试.xlsx", "UTF-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
        ServletOutputStream outputStream = response.getOutputStream();
        ArrayList<Task> tasks = new ArrayList<>();
        Task task = new Task();
        task.setVin("123");
        task.setTaskInfo("123");
        tasks.add(task);
        EasyExcel.write(outputStream, Task.class).sheet("Sheet").doWrite(tasks);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
ExcelWriter excelWriter = EasyExcel.write(outputStream, Task.class).build();
WriteSheet sheet = EasyExcel.writerSheet("Sheet").build();
excelWriter.write(tasks, sheet);
excelWriter.finish();

不使用注解的话就使用head方法指定列表头。

指定导出列

使用注解。或者:

EasyExcel.write().excludeColumnFiledNames(List)
EasyExcel.write().includeColumnFiledNames(List)

ExcelProperty注解的index属性可以指定这一列在Excel中的位置。数字从0开始。

复杂头写入

例如:

java word 导入导出工具类 java文件导入导出插件_List


只需要使用注解ExcelProperty

@ExcelProperty(value = {"任务", "*车架号"})
private String vin;

@ExcelProperty(value = {"任务", "*任务信息"})
private String taskInfo;

日期、数字或自定义格式转换对导出数据进行处理

自定义样式

例:@NumberFormat@DateTimeFormat

try {
    response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.setCharacterEncoding("UTF-8");
    String fileName = URLEncoder.encode("测试.xlsx", "UTF-8");
    response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
    ServletOutputStream outputStream = response.getOutputStream();
    ArrayList<Task> tasks = new ArrayList<>();
    Task task = new Task();
    task.setVin("123");
    task.setTaskInfo("123");
    tasks.add(task);
    //头的策略
    WriteCellStyle headWriteCellStyle = new WriteCellStyle();
    // 背景设置为红色
    headWriteCellStyle.setFillForegroundColor(IndexedColors.BLACK.getIndex());
    WriteFont headWriteFont = new WriteFont();
    headWriteFont.setFontHeightInPoints((short) 20);
    headWriteFont.setColor(IndexedColors.RED.getIndex());
    headWriteCellStyle.setWriteFont(headWriteFont);
    // 内容的策略
    WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
    // 这里需要指定 FillPatternType 为FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色.头默认了 FillPatternType所以可以不指定
    contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND);
    // 背景绿色
    contentWriteCellStyle.setFillForegroundColor(IndexedColors.BLUE.getIndex());
    WriteFont contentWriteFont = new WriteFont();
    // 字体大小
    contentWriteFont.setFontHeightInPoints((short) 20);
    contentWriteCellStyle.setWriteFont(contentWriteFont);
    // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
    HorizontalCellStyleStrategy horizontalCellStyleStrategy =
            new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
    ExcelWriter excelWriter = EasyExcel.write(outputStream, Task.class).registerWriteHandler(horizontalCellStyleStrategy).build();
    WriteSheet sheet = EasyExcel.writerSheet("Sheet").build();
    excelWriter.write(tasks, sheet);
    excelWriter.finish();
} catch (IOException e) {
    throw new RuntimeException(e);
}

合并单元格

// 每隔2行会合并 把eachColumn 设置成 3 也就是我们数据的长度,所以就第一列会合并。当然其他合并策略也可以自己写
LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0);

动态列表头

//列表头
List<List<String>> headList = new ArrayList<>();
//因为可能会有复杂列表头所以每一列的头都是一个List
List<String> head1 = new ArrayList<>();
head1.add("第一行头");
head1.add("第二行头");
headList.add(head1);
EasyExcel.write(outputStream).head(headList).sheet("Sheet").doWrite(tasks);

java word 导入导出工具类 java文件导入导出插件_List_02

自定义拦截器,对单元格进行操作

/**
 * 自定义拦截器。对第一行第一列的头超链接到:https://github.com/alibaba/easyexcel
 */
public class CustomCellWriteHandler implements CellWriteHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomCellWriteHandler.class);

    @Override
    public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row,
        Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell,
        Head head, Integer relativeRowIndex, Boolean isHead) {

    }

    @Override
    public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder,
        List<CellData> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        // 这里可以对cell进行任何操作
        LOGGER.info("第{}行,第{}列写入完成。", cell.getRowIndex(), cell.getColumnIndex());
        if (isHead && cell.getColumnIndex() == 0) {
            CreationHelper createHelper = writeSheetHolder.getSheet().getWorkbook().getCreationHelper();
            Hyperlink hyperlink = createHelper.createHyperlink(HyperlinkType.URL);
            hyperlink.setAddress("https://github.com/alibaba/easyexcel");
            cell.setHyperlink(hyperlink);
        }
    }
}
/**
 * 自定义拦截器.对第一列第一行和第二行的数据新增下拉框,显示 测试1 测试2
 */
public class CustomSheetWriteHandler implements SheetWriteHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(CustomSheetWriteHandler.class);

    @Override
    public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {

    }

    @Override
    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
        LOGGER.info("第{}个Sheet写入成功。", writeSheetHolder.getSheetNo());

        // 区间设置 第一列第一行和第二行的数据。由于第一行是头,所以第一、二行的数据实际上是第二三行
        CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(1, 2, 0, 0);
        DataValidationHelper helper = writeSheetHolder.getSheet().getDataValidationHelper();
        DataValidationConstraint constraint = helper.createExplicitListConstraint(new String[] {"测试1", "测试2"});
        DataValidation dataValidation = helper.createValidation(constraint, cellRangeAddressList);
        writeSheetHolder.getSheet().addValidationData(dataValidation);
    }
}

使用registerWriteHandler方法将上面的自定义拦截器注册即可。

工具类

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.byd.demo.config.ExcelListener;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Component
public class ExcelUtils {

    /**
     * 从 excel 文件中读取数据
     *
     * @param inputStream 文件输入流
     * @param sheetNo     工作表序号,从 0 开始
     * @param clazz       数据对应的类类型
     * @return 读取的数据列表
     */
    public static List<Object> readExcel(InputStream inputStream, int sheetNo,
                                         Class<?> clazz) {
        ExcelListener<Object> listener = new ExcelListener<>();
        ExcelReaderBuilder readerBuilder = EasyExcel.read(inputStream, clazz, listener);
        readerBuilder.sheet(sheetNo).doRead();
        return listener.getRows();
    }

    /**
     * 将数据写入 excel 文件并下载
     *
     * @param response  HttpServletResponse 对象
     * @param fileName  下载的文件名(不包含后缀)
     * @param sheetName 工作表名称
     * @param dataList  数据列表
     * @throws IOException IO 异常
     */
    public static void writeExcel(HttpServletResponse response, String fileName, String sheetName,
                                  List<?> dataList) throws IOException {
        response.setContentType("application/vnd.ms-excel");
        response.setCharacterEncoding("utf-8");
        //处理文件名乱码
        fileName = URLEncoder.encode(fileName, "UTF-8");
        response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");

        Workbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet(sheetName);

        // 写入表头
        int rowIndex = 0;
        Row headerRow = sheet.createRow(rowIndex++);
        buildHeader(headerRow, dataList.get(0).getClass());

        // 写入数据
        for (Object data : dataList) {
            Row row = sheet.createRow(rowIndex++);
            buildRow(row, data);
        }

        // 输出到 HttpServletResponse
        OutputStream outputStream = response.getOutputStream();
        ExcelWriterBuilder writerBuilder = EasyExcel.write(outputStream);
        writerBuilder.sheet(sheetName).doWrite(dataList);

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

    /**
     * 从 MultipartFile 中读取数据
     *
     * @param file    MultipartFile 对象
     * @param sheetNo 工作表序号,从 0 开始
     * @param clazz   数据对应的类类型
     * @return 读取的数据列表
     * @throws IOException IO 异常
     */
    public static List<Object> readExcel(MultipartFile file, int sheetNo,
                                         Class<?> clazz) throws IOException {
        InputStream inputStream = file.getInputStream();
        return readExcel(inputStream, sheetNo, clazz);
    }

    /**
     * 构建表头行
     *
     * @param headerRow 表头行对象
     * @param clazz     数据类型
     */
    private static void buildHeader(Row headerRow, Class<?> clazz) {
        List<Field> fields = getAllFields(clazz);
        int cellIndex = 0;
        for (Field field : fields) {
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            if (excelProperty != null) {
                Cell cell = headerRow.createCell(cellIndex++);
                cell.setCellValue(excelProperty.value()[0]);
            }
        }
    }

    /**
     * 构建数据行
     *
     * @param row  行对象
     * @param data 数据对象
     */
    private static void buildRow(Row row, Object data) {
        List<Field> fields = getAllFields(data.getClass());
        int cellIndex = 0;
        for (Field field : fields) {
            field.setAccessible(true);
            ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
            if (excelProperty != null) {
                try {
                    Object value = field.get(data);
                    Cell cell = row.createCell(cellIndex++);
                    setCellValue(cell, value);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 获取类及其父类的所有字段(不包括Object类)
     *
     * @param clazz 类类型
     * @return 所有字段列表
     */
    private static List<Field> getAllFields(Class<?> clazz) {
        List<Field> fields = new ArrayList<>();
        while (clazz != null && !clazz.equals(Object.class)) {
            fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
            clazz = clazz.getSuperclass();
        }
        return fields;
    }

    /**
     * 设置单元格的值
     *
     * @param cell  单元格对象
     * @param value 值对象
     */
    private static void setCellValue(Cell cell, Object value) {
        if (value == null) {
            cell.setCellValue("");
            return;
        }

        if (value instanceof Number) {
            cell.setCellValue(((Number) value).doubleValue());
        } else if (value instanceof Boolean) {
            cell.setCellValue((Boolean) value);
        } else if (value instanceof RichTextString) {
            cell.setCellValue((RichTextString) value);
        } else {
            cell.setCellValue(value.toString());
        }
    }
}