Java Excel的数据导入导出

EasyExcel是基于ApachePOI的Java库,它通过SAX模式提高读写性能,尤其适合大数据量处理。它支持使用注解定义导出和导入格式,提供监听器机制在解析数据时实时处理。文章还展示了如何处理不同类型的监听器、读取和写入Excel数据,以及如何与Lombok结合使用。此外,还介绍了EasyExcel的参数设置和错误排查方法。

引入依赖

<!-- EasyExcel -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.2.7</version>
</dependency>

<!--csv文件操作-->
<dependency>
  <groupId>com.opencsv</groupId>
  <artifactId>opencsv</artifactId>
  <version>5.7.1</version>
</dependency>

OpenCSV简介

OpenCSV 是一个成熟的 Java CSV 解析库,支持使用注解将 CSV 文件的列映射到 Java 对象的字段,使用 @CsvBindByName 注解可以快速实现字段映射。支持读写操作,适合导入导出场景。

实体类并使用注解映射字 

import com.opencsv.bean.CsvBindByName;

public class User {
    @CsvBindByName(column = "Name")
    private String name;

    @CsvBindByName(column = "Email")
    private String email;
}

读取 CSV 文件

Reader reader = new FileReader("users.csv");
CsvToBean<User> csvToBean = new CsvToBeanBuilder<User>(reader)
        .withType(User.class)
        .build();

List<User> users = csvToBean.parse();

写入 CSV 文件

import com.opencsv.bean.CsvBeanToBeanBuilder;
import com.opencsv.bean.CsvToBeanBuilder;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.List;

public class CsvWriterExample {
    public static void main(String[] args) throws Exception {
        List<User> users = new ArrayList<>();
        users.add(new User("Alice", "alice@example.com"));
        users.add(new User("Bob", "bob@example.com"));

        try (FileWriter writer = new FileWriter("users_output.csv")) {
            new CsvBeanToBeanBuilder<User>(writer)
                    .withType(User.class)
                    .build()
                    .write(users);
        }
    }
}

EasyExcel简介

easyexcel 是阿里巴巴开源的一款excel 解析工具,底层逻辑也是基于apache poi进行二次开发的。不同的是,再读写数据的时候,采用 sax 模式一行一行解析,并将一行的解析结果以观察者的模式通知处理(AnalysisEventListener)在并发量很大的情况下,依然能稳定运行!easyexcel支持采用注解方式进行导出、导入!

AnalysisEventListener读取监听

对象命名时严格遵守驼峰命名法,如果忽略可能会导致读取数据失败。 

通用读取监听类

import com.baomidou.mybatisplus.toolkit.ReflectionKit;

/**
 * excel通用读取监听类
 */
public class ExcelListener<T> extends AnalysisEventListener<T> {
  /**
   * 自定义用于暂时存储data 可以通过实例获取该值
   */
  private final List<T> list = new ArrayList<>();
  
  /**
   * 这个每一条数据解析都会来调用
   *
   * @param data
   * @param context
   */
  @Override
  public void invoke(T data, AnalysisContext context) {
    // 如果一行Excel数据均为空值,则不装载该行数据
    if (isLineNullValue(data)) {
      return;
    }
    if (context.readRowHolder().getRowIndex() > 2) {
      System.out.println("解析到一条数据:{}", JSON.toJSONString(data));
      list.add(data);
    }
  }

  /**
   * 所有数据解析完成了 都会来调用
   *
   * @param context
   */
  @Override
  public void doAfterAllAnalysed(AnalysisContext context) {
    String sheetName = context.readWorkbookHolder().getExcelType().equals(ExcelTypeEnum.CSV) ? "CSV" : context.readSheetHolder().getSheetName();
    System.out.println(sheetName + " 所有数据解析完成,数据条数:" + list.size());
  }

  /**
   * 返回所有记录
   *
   * @return
   */
  public List<T> getRows() {
    return list;
  }

  /**
   * 判断整行单元格数据是否均为空
   */
  private boolean isLineNullValue(Object data) {
    if (data == null) {
      return true;
    }
    List<Field> fields = Arrays.stream(data.getClass().getDeclaredFields())
        .filter(f -> f.isAnnotationPresent(ExcelProperty.class))
        .collect(Collectors.toList());
    List<Boolean> lineNullList = new ArrayList<>(fields.size());
    for (Field key : fields){
      if (ReflectionKit.getMethodValue(data, key.getName()) == null) {
        lineNullList.add(Boolean.TRUE);
      } else {
        lineNullList.add(Boolean.FALSE);
      }
    }
    return lineNullList.stream().allMatch(Boolean.TRUE::equals);
  }
}

通用非model类型监听

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class NoModelDataListener extends AnalysisEventListener<Map<Integer, String>> {

  /**
   * Excel数据缓存结构
   */
  private List<Map<String, String>> list = new ArrayList<>();

  /**
   * Excel表头(列名)数据缓存结构
   */
  private Map<Integer, String> headTitleMap = new HashMap<>();

  @Override
  public void invoke(Map<Integer, String> data, AnalysisContext context) {
    //excel的行号
    Integer rowIndex = context.readRowHolder().getRowIndex();
    log.info("行号:{},Excel数据:{}", rowIndex, getRowContentWithHeaderKey(data));
    list.add(getRowContentWithHeaderKey(data));
  }

  /**
   * 解析表头数据,解析第一行触发
   **/
  @Override
  public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
    headTitleMap = new HashMap<>();
    for (Integer i : headMap.keySet()) {
      String value = "";
      if (headMap.get(i) != null) {
        value = headMap.get(i).trim();
      }
      headTitleMap.put(i, value);
    }
  }

  @Override
  public void doAfterAllAnalysed(AnalysisContext analysisContext) {
    log.info("数据解析完成!");
  }

  /**
   * 按照表头作为map的key,提供Excel行数据。兼容key重复。行乱序
   *
   * @param data
   * @return
   */
  public Map<String, String> getRowContentWithHeaderKey(Map<Integer, String> data) {
    Map<String, String> dataWithHeader = new IdentityHashMap<>();
    data.forEach((index, value) -> {
      dataWithHeader.put(headTitleMap.get(index), value);
    });
    return dataWithHeader;
  }

  public List<Map<String, String>> getList() {
    return list;
  }
}

单sheet海量excel的数据读取

根据设置每批次数据量的大小,当list里面数据量达到设置每批次最大数据量时,进行入库操作,海量数据的话,通常每批次1000的速度最快,可以避免内存溢出

public void invoke(CallExcelVO data, AnalysisContext context) {
	list.add(data);
	// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
	if (list.size() >= BATCH_COUNT) {
		saveData();
		//存储完成清理 list
		list.clear();
	}
}

 读取Excel内容

ExcelListener listener = new ExcelListener();
String fileName = "demo.xlsx";
EasyExcel.read(fileName, DemoData.class, listener).sheet().autoTrim(true).doRead();
List<DemoData> dataList = listener.getRows();

//EasyExcel3.3 导入csv
ExcelListener listener = new ExcelListener();
ExcelReader excelReader = EasyExcel.read(inputStream, listener).build();
String code = CsvReader.getFileCharset(fileName);
System.out.println("文件编码为:" + code);
//excelReader = EasyExcel.read(inputStream, listener).excelType(ExcelTypeEnum.CSV).charset(Charset.forName(code)).build();
inputStream.close();


Integer sheetSize = 1;//csv
if (excelReader != null) {
    List<ReadSheet> readSheets = excelReader.excelExecutor().sheetList();
    sheetSize = readSheets.size();
}
for (int sheetNo = 0; sheetNo < sheetSize; sheetNo++) {
   ExcelUtil.readExcel(fileName, sheetNo);
}

接收外部参数,需添加构造方法

public ExcelListener(String fileName) {
  this.fileName = fileName;
}

ExcelListener parseListener = new ExcelListener("a.xls");

读类

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import java.io.InputStream;
import java.util.List;

/**
 * 定制化Excel读写工具类
 */
public class ExcelUtil {

  /**
   * 单sheet版本Excel读取 从Excel中读取文件,读取的文件是一个DTO类
   *
   * @param inputStream 文件流
   * @param clazz       行数据类型
   * @param rowIndex    表示Excel的表头的行数,默认为1,设置为1时,表示第一行是表头,从第二行是表数据
   * @param sheetNo     sheet编号 从0开始
   */
  public static <T> List<T> readExcelOneSheet(InputStream inputStream, final Class<?> clazz, Integer rowIndex, Integer sheetNo) {
    // 1.创建监听类
    ExcelListener<T> listener = new ExcelListener<>();
    // 2.构建工作簿对象的输入流
    ExcelReader excelReader = EasyExcel.read(inputStream, clazz, listener).build();
    // 3.构建工作表对象的输入流,默认是第一张工作表
    ReadSheet readSheet = EasyExcel.readSheet(sheetNo).headRowNumber(rowIndex).build();
    // 4.读取信息,每读取一行都会调用监听类的 invoke 方法
    excelReader.read(readSheet);
    // 5.关闭流,如果不关闭,读的时候会创建临时文件,到时磁盘会崩的
    excelReader.finish();
    return listener.getRows();
  }

  /**
   * 将Excel中的日期数值转换为对应的日期字符串,单元格格式设置为文本可以看数字。 45457.4227662037 => 2024-06-14 10:08:46.999
   *
   * @param excelDate Excel中的日期数值,表示从1900年1月1日起的天数与时间的组合。
   * @return 对应的日期字符串,格式为yyyy-MM-dd HH:mm:ss。
   */
  public static String getExcelDateFormat2Date(double excelDate) {
    Calendar calendar = Calendar.getInstance();

    //整数部分代表天数(天数取决于所用的日期系统,1900或1904系统)
    calendar.set(1900, Calendar.JANUARY, 0);
    calendar.add(Calendar.DAY_OF_MONTH, (int) excelDate - 1);

    // Calculate the time portion of the day
    double fractionOfDay = excelDate - (int) excelDate;
    int hours = (int) (fractionOfDay * 24);
    int minutes = (int) ((fractionOfDay * 24 * 60) % 60);
    int seconds = (int) ((fractionOfDay * 24 * 60 * 60) % 60);
    int milliseconds = (int) (((fractionOfDay * 24 * 60 * 60 * 1000) % 1000));

    calendar.set(Calendar.HOUR_OF_DAY, hours);
    calendar.set(Calendar.MINUTE, minutes);
    calendar.set(Calendar.SECOND, seconds);
    calendar.set(Calendar.MILLISECOND, milliseconds);

    Date date = calendar.getTime();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
    return sdf.format(date);
  }
}

EasyExcel导入invoke函数不执行,不报错

  • 重写hasNext方法时写成返回false了
  • 使用easyexcel时默认是第一张sheet页,不是第一张sheet
  • 将不使用的字段使用@ExcelIgnore标注 
  • EasyExcel和Lombok结合使用导致的问题,去掉@Accessors(chain = true)

EasyExcel 相关参数

  • readListener 监听器,在读取数据的过程中会不断的调用监听器。
  • converter 转换器,默认加载了很多转换器。也可以自定义,如果使用的是 registerConverter,那么该转换器是全局的,如果要对单个字段生效,可以在 ExcelProperty 注解的 converter 指定转换器。
  • headRowNumber 需要读的表格有几行头数据。默认有一行头,也就是认为第二行开始起为数据。
  • head 与 clazz 二选一。读取文件头对应的列表,会根据列表匹配数据,建议使用 class。
  • autoTrim 字符串、表头等数据自动 trim
  • sheetNo 需要读取 Sheet 的编码,建议使用这个来指定读取哪个 Sheet。
  • sheetName 根据名字去匹配 Sheet,excel 2003 不支持根据名字去匹配。 

Excel工具类

ExportUtil导出导入

CSV反射支持easyexcel获取ExcelProperty

package net.demo.excel.common.util;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.opencsv.CSVWriter;
import com.opencsv.bean.StatefulBeanToCsv;
import com.opencsv.bean.StatefulBeanToCsvBuilder;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.ArrayList;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import java.io.OutputStreamWriter;
import java.io.Writer;
import org.springframework.web.multipart.MultipartFile;
import com.opencsv.CSVReader;
import com.opencsv.bean.CsvToBean;
import com.opencsv.bean.CsvToBeanBuilder;

/**
 * Excel/csv 导入导出工具类
 *
 * @author hudeyong
 * @date 2025/7/15
 */
public class ExportUtil {
  
  /**
   * 获取指定类中所有标记为ExcelProperty的字段名
   * 
   * @param tClass 要检查的类
   * @return 字段名数组
   */
  public static <T> String[] getFieldNames(Class<T> tClass) {
    // 获取类的所有声明的字段
    Field[] fields = tClass.getDeclaredFields();
    List<String> headers = new ArrayList<>();
    for (Field field : fields) {
      // 检查字段是否被ExcelProperty注解标记
      ExcelProperty property = field.getAnnotation(ExcelProperty.class);
      if (property != null) {
        // 获取注解的值,即字段名
        String[] s = property.value();
        if (s.length > 0) {
          headers.add(s[0]);
        }
      }
    }
    // 将字段名列表转换为数组并返回
    String[] strings = new String[headers.size()];
    headers.toArray(strings);
    return strings;
  }
  
  /**
   * 获取指定对象中所有标记为ExcelProperty的字段的值
   * 
   * @param vo 要检查的对象
   * @return 字段值数组
   */
  public static <T> String[] getFields(T vo) {
    // 获取对象的所有声明的字段
    Field[] fields = vo.getClass().getDeclaredFields();
    List<String> columns = new ArrayList<>();
    for (Field field : fields) {
      // 检查字段是否被ExcelProperty注解标记
      ExcelProperty property = field.getAnnotation(ExcelProperty.class);
      if (property != null) {
        try {
          // 设置字段可访问,以读取私有字段的值
          field.setAccessible(true);
          // 获取字段值并转换为字符串,如果值为null,则使用空字符串
          columns.add(field.get(vo) == null ? "" : field.get(vo).toString());
        } catch (IllegalAccessException e) {
          // 如果访问字段失败,打印异常信息并抛出运行时异常
          e.printStackTrace();
          throw new RuntimeException("写入内容到csv失败!");
        }
      }
    }
    // 将字段值列表转换为数组并返回
    String[] strings = new String[columns.size()];
    columns.toArray(strings);
    return strings;
  }


  /**
   * 导出Csv文件使用ExcelProperty 注解,从而实现与 EasyExcel 类似的字段映射功能,简化开发工作。
   *
   * @param response
   * @param dataList
   * @param clazz
   * @param fileName
   */
  public static void exportCsvByExcelProperty(HttpServletResponse response, List<?> dataList, Class<?> clazz, String fileName) {
    try {
      // 设置响应头
      response.setContentType("text/csv");
      response.setCharacterEncoding("utf-8");

      // 对文件名进行编码处理
      fileName = URLEncoder.encode(fileName + DateUtils.getCurrentTimestamp(), "UTF-8").replaceAll("\\+", "%20");
      response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".csv");

      // 使用 OpenCSV 写入数据
      CSVWriter writer = new CSVWriter(new OutputStreamWriter(response.getOutputStream(), "UTF-8"));
      // 写入表头
      String[] headers = getFieldNames(clazz);
      writer.writeNext(headers);
      // 写入数据
      for (Object data : dataList) {
        String[] fields = getFields(data);
        writer.writeNext(fields);
      }

      writer.flush();
      writer.close();
    } catch (Exception e) {
      try {
        response.reset();
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().println("导出csv失败: " + e.getMessage());
      } catch (IOException ex) {
        ex.printStackTrace();
      }
    }
  }

  /**
   * 根据 CSV 文件解析数据
   *
   * @param file  上传的 CSV 文件
   * @param clazz 解析的目标 DTO 类
   * @return 解析后的数据列表
   */
  public static <T> List<T> importCsv(MultipartFile file, Class<T> clazz) {
    if (file == null || file.isEmpty()) {
      throw new RuntimeException("没有文件或者文件内容为空!");
    }

    try (InputStream is = file.getInputStream();
        CSVReader reader = new CSVReader(new InputStreamReader(is, "UTF-8"))) {

      CsvToBean<T> csvToBean = new CsvToBeanBuilder<T>(reader)
          .withType(clazz)
          .withIgnoreLeadingWhiteSpace(true)
          .build();

      return csvToBean.parse();

    } catch (Exception e) {
      throw new RuntimeException("CSV 数据解析失败: " + e.getMessage(), e);
    }
  }

  /**
   * 导出 CSV 文件
   *
   * @param response 响应对象
   * @param dataList 要导出的数据列表
   * @param clazz    数据对应的 DTO 类
   * @param fileName  文件名
   */
  public static void exportCsv(HttpServletResponse response, List<?> dataList, Class<?> clazz, String fileName) {
    try {
      // 设置响应头
      response.setContentType("text/csv");
      response.setCharacterEncoding("utf-8");

      // 对文件名进行编码处理
      fileName = URLEncoder.encode(fileName + DateUtils.getCurrentTimestamp(), "UTF-8").replaceAll("\\+", "%20");
      response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".csv");

      // 使用 OpenCSV 写入数据
      Writer writer = new OutputStreamWriter(response.getOutputStream(), "UTF-8");
      StatefulBeanToCsv<Object> beanToCsv = new StatefulBeanToCsvBuilder<>(writer)
          .withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
          .build();

      // 写入数据
      for (Object data : dataList) {
        beanToCsv.write(data);
      }

      writer.flush();
      writer.close();
    } catch (Exception e) {
      try {
        response.reset();
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().println("导出csv失败: " + e.getMessage());
      } catch (IOException ex) {
        ex.printStackTrace();
      }
    }
  }

  /**
   * 导出 Excel 文件
   *
   * @param response 响应对象
   * @param dataList 要导出的数据列表
   * @param clazz    数据对应的 DTO 类
   * @param fileName 文件名
   */
  public static void exportExcel(HttpServletResponse response, List<?> dataList, Class<?> clazz, String fileName) {
    try {
      // 设置响应头
      response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
      response.setCharacterEncoding("utf-8");

      String sheetName = fileName;
      // 对文件名进行编码处理
      fileName = URLEncoder.encode(fileName + DateUtils.getCurrentTimestamp(), "UTF-8").replaceAll("\\+", "%20");

      // 设置 Content-Disposition 头,指定下载的文件名和类型
      response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");

      // 获取所有无 ExcelProperty 注解的字段名
      List<String> excludedFields = new ArrayList<>();
      for (Field field : clazz.getDeclaredFields()) {
        if (field.getAnnotation(ExcelProperty.class) == null) {
          excludedFields.add(field.getName());
        }
      }

      // 使用 EasyExcel 写入数据,并排除无注解字段
      ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream(), clazz).excludeColumnFiledNames(excludedFields).build();
      WriteSheet writeSheet = EasyExcel.writerSheet(sheetName).build();

      // 分批写入数据
      int batchSize = 5000;
      for (int i = 0; i < dataList.size(); i += batchSize) {
        int end = Math.min(i + batchSize, dataList.size());
        excelWriter.write(dataList.subList(i, end), writeSheet);
      }

      // 关闭流
      excelWriter.finish();
    } catch (IOException e) {
      // 处理异常
      try {
        response.reset();
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().println("导出失败: " + e.getMessage());
      } catch (IOException ex) {
        ex.printStackTrace();
      }
    }
  }

  /**
   * 根据Excel模板,批量导入数据
   *
   * @param file  导入的Excel
   * @param clazz 解析的类型
   * @return 解析完成的数据
   */
  public static List<?> importExcel(MultipartFile file, Class<?> clazz) {
    if (file == null || file.isEmpty()) {
      throw new RuntimeException("没有文件或者文件内容为空!");
    }
    List<Object> dataList = null;
    BufferedInputStream ipt = null;
    try {
      InputStream is = file.getInputStream();
      // 用缓冲流对数据流进行包装
      ipt = new BufferedInputStream(is);
      // 数据解析监听器
      ExcelListener<Object> listener = new ExcelListener<>();
      // 读取数据
      EasyExcel.read(ipt, clazz, listener).sheet().doRead();
      // 获取去读完成之后的数据
      dataList = listener.getDataList();
    } catch (Exception e) {
      log.error(String.valueOf(e));
      throw new RuntimeException("数据导入失败!" + e);
    }
    return dataList;
  }
}

CsvReader文件读取

import com.alibaba.excel.util.FileUtils;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;

/**
 * CSV读取工具类
 */
public class CsvReader {

  /**
   * 列分隔符
   */
  private static final String WORDS_SPLIT = ",";

  /**
   * 读取200万条数据
   */
  private static final int READ_LINES = Integer.MAX_VALUE;

  /**
   * csv、txt 文件处理
   *
   * @param filePath 文件路径
   * @return
   */
  public static Map<Integer, Map<Integer, String>> getTxtCsvFileContent(String filePath) {
    Map<Integer, Map<Integer, String>> map = new HashMap<>();
    try {
      Map<Integer, String> rowContent = getTxtCsvLineContentMap(
          FileUtils.openInputStream(new File(filePath)), filePath, READ_LINES);
      if (rowContent != null) {
        rowContent.forEach((k, v) -> {
          Map<Integer, String> tmp = new LinkedHashMap<>();
          String[] row = v.split(WORDS_SPLIT);
          for (int i = 0; i < row.length; i++) {
            tmp.put(i, row[i]);
          }
          map.put(k, tmp);
        });
      }
    } catch (IOException e) {
      e.printStackTrace();
    }

    return map;
  }

  /**
   * 兼容Excel多sheet格式
   *
   * @param filePath
   * @return
   */
  public static List<Map<Integer, Map<Integer, String>>> getExcelStyleData(String filePath) {
    List sheet = new ArrayList<>();
    Map<Integer, Map<Integer, String>> sheetData = getTxtCsvFileContent(filePath);
    sheet.add(0, sheetData);
    return sheet;
  }

  /**
   * csv、txt 文件第一行表头
   *
   * @param filePath 文件路径
   * @param charset
   * @return
   */
  public static Map<Integer, String> getTxtCsvFileHeader(String filePath, String charset) {
    Map<Integer, String> headerMap = new LinkedHashMap<>();

    try {
      if (StringUtils.isBlank(charset)) {
        charset = getFileCharset(filePath);
        if (StringUtils.isBlank(charset)) {
          charset = StandardCharsets.UTF_8.name();
        }
      }
      InputStream is = new BufferedInputStream(FileUtils.openInputStream(new File(filePath)));
      // 解析平面文件
      BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset));
      // 获取第一行的数据当做字段
      String headContent = reader.readLine();
      if (StringUtils.isNotBlank(headContent)) {
        if (isMessyCode(headContent, Charset.forName(charset)) && !charset.equals(StandardCharsets.UTF_8.name())) {
          System.out.println("文件编码:" + charset + ",内容存在乱码,自动修正编码为utf8");
          is.close();
          reader.close();
          return getTxtCsvFileHeader(filePath, StandardCharsets.UTF_8.name());
        }
        String[] row = headContent.split(WORDS_SPLIT);
        for (int i = 0; i < row.length; i++) {
          headerMap.put(i, row[i]);
        }
      }
      is.close();
      reader.close();
    } catch (IOException e) {
      e.printStackTrace();
    }

    return headerMap;
  }

  /**
   * 获取 txt/csv 文件前 size 行数据
   *
   * @param inputStream
   * @param filePath
   * @param size
   * @return
   * @throws IOException
   */
  public static Map<Integer, String> getTxtCsvLineContentMap(InputStream inputStream,
      String filePath, int size) throws IOException {
    Map<Integer, String> map = new LinkedHashMap<>();
    String charset = getFileCharset(filePath);
    if (StringUtils.isBlank(charset)) {
      charset = StandardCharsets.UTF_8.name();
    }
    InputStream is = new BufferedInputStream(inputStream);
    // 解析平面文件
    BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset));
    // 获取第一行的数据当做字段
    String line = reader.readLine();
    if (StringUtils.isBlank(line)) {
      is.close();
      reader.close();
      return null;
    }
    int fileLineNum = getFileLineNum(filePath);
    if (fileLineNum < size) {
      size = fileLineNum;
    }
    for (int i = 1; i <= size; i++) {
      String nowLineContent = reader.readLine();
      nowLineContent = StringUtils.remove(nowLineContent, "\"");
      nowLineContent = StringUtils.remove(nowLineContent, "\t");
      Integer lineIndex = i + 1;
      if (StringUtils.isNotBlank(nowLineContent)) {
        if (lineIndex % 200 == 0) {
          System.out.println("读取csv第" + lineIndex + "/" + size + "内容:" + nowLineContent);
        }
        map.put(lineIndex, nowLineContent);
      }
    }
    is.close();
    reader.close();
    return map;
  }

  /**
   * 高效获取大文件的行数:3千万行只要1秒钟
   *
   * @param filePath
   * @return int
   */
  public static int getFileLineNum(String filePath) {
    int fileLines = 0;
    try (LineNumberReader lineNumberReader = new LineNumberReader(new FileReader(filePath))) {
      lineNumberReader.skip(Long.MAX_VALUE);
      int lineNumber = lineNumberReader.getLineNumber();
      //实际上是读取换行符数量 , 所以需要+1
      fileLines = lineNumber + 1;
    } catch (IOException e) {
      e.printStackTrace();
    }
    return fileLines;
  }

  /**
   * 根据关键字获取表头
   *
   * @param filePath
   * @param headerKey
   * @return
   */
  public static Map<Integer, String> getTxtCsvFileHeaderContainsKeywords(String filePath,
      String headerKey) {
    Map<Integer, String> headerMap = new LinkedHashMap<>();
    try {
      InputStream inputStream = new FileInputStream(filePath);
      Map<Integer, String> tmp = CsvReader.getTxtCsvLineContentMap(inputStream, filePath, 50);
      for (Map.Entry<Integer, String> entry : tmp.entrySet()) {
        String[] row = entry.getValue().split(CsvReader.WORDS_SPLIT);
        if (row.length > 0) {
          row[0] = StringUtils.remove(row[0], "\"");
          row[0] = StringUtils.remove(row[0], "\t");
          String title = StringUtils.deleteWhitespace(row[0]);
          for (int i = 0; i < row.length; i++) {
            if (title.equals(headerKey)) {
              headerMap.put(i, row[i]);
            }
          }
          if (title.equals(headerKey)) {
            break;
          }
        }
      }
      inputStream.close();
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return headerMap;
  }

  /**
   * 获取 txt/csv/dat 文件编码
   *
   * @param filePath 文件路径
   * @return
   */
  private static String getFileCharset(String filePath) {
    String charset = "UTF-8";

    byte[] bytes = new byte[3];
    try (InputStream inputStream = new FileInputStream(filePath)) {
      inputStream.read(bytes);
      if (bytes[0] == (byte) 0xEF && bytes[1] == (byte) 0xBB && bytes[2] == (byte) 0xBF) {
        charset = "UTF-8";
      } else if (bytes[0] == (byte) 0xFE && bytes[1] == (byte) 0xFF) {
        charset = "UTF-16BE";
      } else if (bytes[0] == (byte) 0xFF && bytes[1] == (byte) 0xFE) {
        charset = "UTF-16LE";
      } else {
        //或GB2312,即ANSI
        charset = "GBK";
      }
      inputStream.close();
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }

    return charset;
  }

  /**
   * 判断在指定编码下是否包含乱码 输出 false,表示不包含乱码字符
   *
   * @param str
   * @param charset
   * @return
   */
  public static boolean isMessyCode(String str, Charset charset) {
    String decodedStr = new String(str.getBytes(charset), charset);
    return !decodedStr.equals(str);
  }
}

创建导入数据模板类

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import lombok.Data;
import lombok.experimental.Accessors;
 
import javax.validation.constraints.NotEmpty;
import java.io.Serializable;
 
/**
 * 数据导入的Excel模板实体
 */
@Data
public class ImportExcelVo implements Serializable {
    private static final long serialVersionUID = 1L;

    //使用index读取解决表头文字频繁变动问题
    @ExcelProperty(index = 0)
    private String name;
 
    @ColumnWidth(20)
    @ExcelProperty(value = "公司联系电话")
    private String phone;
 
    @ColumnWidth(28)
    @ExcelProperty(value = "公司统一社会信用代码")
    private String creditCode;
 
    @ColumnWidth(15)
    @ExcelProperty(value = "区域", index = 3)
    private String province;
 
    @ColumnWidth(15)
    @ExcelProperty(value = "公司法人")
    private String legalPerson;
 
    @ExcelProperty(value = "备注")
    private String remark;

    @DateTimeFormat("yyyy-MM-dd")  //指定日期格式
    private Date date;
}

invoke时间格式,需要使用easyExcel中的@ExcelProperty注解,并指定日期格式

  public void invoke(Object data, AnalysisContext context) {
    // 读取日期属性并进行处理
    if (rowData.get(columnIndex) instanceof Date) {
      Date date = (Date) rowData.get(columnIndex);
      // 在这里对日期进行处理
      SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
      String dateStr = sdf.format(date);
      //其他属性的处理...
      System.out.println(data);
    }
  }

ExcelProperty注解

可以定义表头的名称。这个注解还提供了index、order两个属性,可以定义列的位置和顺序。

  •  1)关于index

如果所有字段都不加index的话,默认index会从0开始,最早的声明的字段的名字的index的值就是0。之后字段的index就从0开始递增,依次类推。index是几决定了该字段数据会赋值给Excel中的第几列。如果不想按这个顺序把数据写到Excel当中。那么就可以手动设置index的值,把字段写到想要的列中去。如果index相同,直接会抛出异常,因为程序无法判断这个列放那个字段。

  • 2)关于order

index和order虽然都决定顺序,但是两者语义不同:如果order和index同时使用,index优先占据位置,order再进行排序。index=-1的话,使用jJava进行默认排序。order的默认值为Integer.MAX_VALUE,其中order的值越小,列越靠前

其他注解

  • @DateTimeFormat 日期转换,用String去接收excel日期格式的数据会调用这个注解。@DateTimeFormat(value = "yyyy-MM-dd HH:mm:ss")指定Excel中的日期类型
  • @NumberFormat 数字转换,用String去接收excel数字格式的数据会调用这个注解。
  • @ColumnWith  设置列宽度。只有一个参数value,value的单位是字符长度,最大可以设置255个字符

@ExcelIgnore

EasyExcel组件标注在成员变量上,默认所有字段都会和excel去匹配,加了这个注解会忽略该字段

使用方法

/**
 * Excel批量导入数据
 *
 * @param file 导入文件
 */
@RequestMapping(value = "/import", method = RequestMethod.POST)
public CommonResponse<String> importEvents(MultipartFile file) {
  try {
    List<?> list = ExportUtil.importExcel(file, ImportExcelVo.class);
    System.out.println(list);
    return CommonResponse.success("数据导入完成");
  } catch (Exception e) {
    return CommonResponse.error("数据导入失败!" + e.getMessage());
  }
}

Excel中日期格式

在Excel中,日期是以所谓的“序列号”存储的,这种序列号实际上是一个浮点数(因此有时被称为“double格式”),它代表了自1900年1月1日以来的天数。整数部分表示天数,而小数部分表示一天内的小时、分钟和秒数。例如,1900年1月1日在Excel中被存储为数字1.0,因为这是序列的起始点。那么,2023年1月1日将被存储为一个较大的数字,因为这一天距离1900年1月1日更远。具体来说,Excel日期序列号的计算方式如下:

  • 整数部分代表天数,例如,2023年1月1日是44622(取决于所用的日期系统,1900或1904系统)。
  • 小数部分代表一天中的时间。例如,0.5表示中午12:00,因为一天有24小时,所以0.5即为24小时的一半。

在大多数编程环境中,程序通常读取单元格的数值内容,而不是其显示的日期格式,因此了解日期规则就能够还原出日期内容

EasyExcel自定义转换器Converter

Timestamp 转换器

import java.sql.Timestamp;
import java.text.SimpleDateFormat;
 
import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

public class TimestampConverter implements Converter<Timestamp> {

  @Override
  public Class<Timestamp> supportJavaTypeKey() {
    return Timestamp.class;
  }

  @Override
  public CellDataTypeEnum supportExcelTypeKey() {
    return CellDataTypeEnum.STRING;
  }

  /**
   * 将excel对象转换为Java对象
   */
  @Override
  public Timestamp convertToJavaData(CellData cellData, ExcelContentProperty contentProperty,
      GlobalConfiguration globalConfiguration) throws Exception {
    return null;
  }


  /**
   * 将 Java 对象转换为 excel 对象
   */
  @Override
  public CellData convertToExcelData(Timestamp timestamp, ExcelContentProperty contentProperty,
      GlobalConfiguration globalConfiguration) throws Exception {
    return new CellData(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(timestamp));
  }
}

NullConverter

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

public class NullConverter implements Converter<String> {

	/**
	 * 回到 Java 中的对象类型
	 *
	 * @return 支持 Java 类
	 */
	@Override
	public Class supportJavaTypeKey() {
		return String.class;
	}

	/**
	 * * 返回 excel 中的对象枚举
	 * * @return 支持 {@link Cell DataTypeEnum}
	 * */
	@Override
	public CellDataTypeEnum supportExcelTypeKey() {
		return CellDataTypeEnum.STRING;
	}

	/**
	 * 将excel对象转换为Java对象
	 *
	 * @param cellData
	 * Excel 单元格数据。NotNull。
	 * @param contentProperty
	 * 内容属性。可空。
	 * @param globalConfiguration
	 * 全局配置。NotNull。
	 * @return 要放入 Java 对象的数据
	 * @抛出异常
	 *             例外。
	 */
	@Override
	public String convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
		return "-".equals(cellData.getStringValue()) ? null : cellData.getStringValue();
	}

	/**
	 * 将 Java 对象转换为 excel 对象
	 *
	 * @参数值
	 * Java 数据.NotNull。
	 * @param contentProperty
	 * 内容属性。可空。
	 * @param globalConfiguration
	 * 全局配置。NotNull。
	 * @return 数据放入 Excel
	 * @抛出异常
	 *             例外。
	 */
	@Override
	public CellData convertToExcelData(String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
		return new CellData<>(null == value ? "-" : value);
	}
}

ExcelToCsvConverter

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;

/**
 * Excel转换CSV的工具类
 *
 * @author hudy
 */
public class ExcelToCsvConverter {

  /**
   * 将Excel文件转换为CSV文件
   *
   * @param sourceFilePath Excel文件路径
   * @param targetFilePath CSV文件输出路径
   */
  public static void convertExcelToCsv(String sourceFilePath, String targetFilePath) {
    // 使用try-with-resources语句确保输入输出流的关闭
    try (InputStream inputStream = new FileInputStream(sourceFilePath);
        OutputStream outputStream = new FileOutputStream(targetFilePath)) {

      // 使用EasyExcel框架读取Excel文件并转换为CSV格式
      EasyExcel.read(inputStream, null, new ExcelToCsvListener(outputStream)).sheet().doRead();

    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * Excel到CSV的监听器,用于处理每行数据并写入CSV
   */
  private static class ExcelToCsvListener extends AnalysisEventListener<Map<Integer, String>> {

    private final OutputStream outputStream;

    public ExcelToCsvListener(OutputStream outputStream) {
      this.outputStream = outputStream;
    }

    /**
     * 处理每行Excel数据,将其写入CSV格式
     */
    @Override
    public void invoke(Map<Integer, String> data, AnalysisContext context) {
      writeData(data, context);
    }

    @Override
    public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
      try {
        outputStream.write(getCsvBom());
        writeData(headMap, context);
      } catch (IOException e) {
        e.printStackTrace();
      }
    }

    /**
     * 将行数据转换为CSV格式并写入输出流
     */
    private void writeData(Map<Integer, String> data, AnalysisContext context) {
      try {
        StringBuilder rowContent = new StringBuilder();
        // 构建CSV行内容
        for (int i = 0; i < data.size(); i++) {
          if (i > 0) {
            rowContent.append(",");
          }
          rowContent.append(data.get(i));
        }
        // 写入行内容到输出流
        if (rowContent.length() > 0) {
          outputStream.write((rowContent.toString() + "\n").getBytes());
        }
        if (context.readRowHolder().getRowIndex() % 100 == 0) {
          System.out.println("解析到Excel行号:" + context.readRowHolder().getRowIndex());
        }
      } catch (IOException e) {
        throw new RuntimeException("写入CSV时发生错误", e);
      }
    }

    /**
     * 防止导出Csv乱码
     *
     * @return
     */
    private byte[] getCsvBom() {
      return new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
    }

    /**
     * 当所有Excel数据被解析完成时调用,用于关闭输出流
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
      try {
        System.out.println("所有数据解析完成!");
        outputStream.flush();
        outputStream.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  // 示例主方法,用于演示类的使用
  public static void main(String[] args) {
    String sourceFilePath = "zjx.xlsx";
    String targetFilePath = "zjx.csv";
    convertExcelToCsv(sourceFilePath, targetFilePath);
  }
}

StatusConverter

import com.test.project.StatusEnum;

import com.alibaba.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.CellData;
import com.alibaba.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.property.ExcelContentProperty;

/**
 * 状态枚举转换器
 */
public class StatusConverter implements Converter<Integer> {

  @Override
  public Class<Integer> supportJavaTypeKey() {
    return Integer.class;
  }

  @Override
  public CellDataTypeEnum supportExcelTypeKey() {
    return CellDataTypeEnum.STRING;
  }

  /**
   * 将excel对象转换为Java对象
   */
  @Override
  public Integer convertToJavaData(CellData cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
    return StatusEnum.getKey(cellData.getStringValue());
  }

  /**
   * 将 Java 对象转换为 excel 对象
   */
  @Override
  public CellData convertToExcelData(Integer integer, ExcelContentProperty excelContentProperty,
      GlobalConfiguration globalConfiguration) throws Exception {
    return new CellData(StatusEnum.getValue(integer));
  }
}

使用方法一

每个字段都要添加@ExcelProperty(converter = NullConverter.class)代码,如果遇到大量的数据字段去填充处理会增加很多工作量。转换器仅支持需要被处理的数据字段,也就是适用于从数据库查询出来已有的数据,如日期格式或性别字段做转换时才生效

使用方法二

File uploadFile = File.createTempFile("export", ".xlsx");
String templateFilePath = systemUrl + "/template/exportPublishShop.xlsx";
 
ExcelWriterSheetBuilder excelWriterSheetBuilder = 
EasyExcel.write(uploadFile).registerConverter(new TimestampConverter()).withTemplate(templateFilePath).sheet();
 
List<Map<String, String>> productList = 查询数据数据
 
// productList 如果数据量很大一定要做分页查询,避免占用内存过大
excelWriterSheetBuilder.doFill(productList);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值