Java学习笔记之——Excel一键导入导出
使用的技术:SpringBoot3.X
,Maven包管理
,MybatisPlus
,mysql数据库
,MybatisX工具自动生成代码功能
,EasyExcel阿里表格处理工具包
,Logback日志输出
文章目录
本文对应仓库以及代码拉取方式:
// 克隆远程仓库
git clone https://gitee.com/strivezhangp/java-study-log.git
// 打开克隆的目录
cd java-study-log
// 选择当前分支
git checkout logindemo
// 拉取当前分支代码
git pull
1 EasyExcel依赖注入
官方使用文档:https://easyexcel.opensource.alibaba.com/docs/current/,首先在,pom.xml文件中引入依赖。
<!-- 阿里EasyExcel工具-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.4</version>
</dependency>
2 EasyExcel与Apache POI 对比
-
内存占用低
EasyExcel 在处理大数据量时内存占用更低,因为它使用了基于SAX的解析模式,逐行读取和写入数据,不需要将整个文件加载到内存中。这使得它在处理大型Excel文件时更加高效,避免了内存溢出的问题。
-
性能更好
由于内存占用低,EasyExcel 的读写性能相对于 Apache POI 更加优越。特别是在处理数十万甚至上百万行数据时,EasyExcel 的表现明显优于 Apache POI。
-
简单易用
EasyExcel 提供了简洁的API,使用起来更加方便。它的API设计更贴近业务需求,易于上手,可以用更少的代码实现同样的功能。
-
支持注解
EasyExcel 支持通过注解来定义Excel列与Java对象属性的映射关系,简化了配置和映射过程。这种方式使得代码更加直观,易于维护。
2.1 简单的示例
Apache POI 示例:
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet("Animals");
Row headerRow = sheet.createRow(0);
String[] headers = {"ID", "Name", "Species", "Age"};
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
}
int rowIdx = 1;
for (TAnimals animal : animalsList) {
Row row = sheet.createRow(rowIdx++);
row.createCell(0).setCellValue(animal.getId());
row.createCell(1).setCellValue(animal.getName());
row.createCell(2).setCellValue(animal.getSpecies());
row.createCell(3).setCellValue(animal.getAge());
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
workbook.close();
EasyExcel 示例:
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ExcelWriterBuilder writerBuilder = EasyExcel.write(outputStream, TAnimals.class);
writerBuilder.sheet("Animals").doWrite(animalsList);
从示例中可以看出,EasyExcel 的代码更加简洁,减少了手动创建单元格和行的代码量。所以,EasyExcel的优缺点,总结见下:
优点:
- 内存占用低:基于SAX的解析模式,适合处理大文件。
- 性能更好:特别是在处理大数据量时表现更加优越。
- 简单易用:API设计简洁,代码量少,容易上手。
- 支持注解:通过注解定义列与属性的映射关系,配置简单。
缺点:
- 功能相对有限:相对于 Apache POI,EasyExcel 在处理某些复杂的Excel功能(如图表、公式等)方面可能不如 POI 强大。
- 文档和社区:虽然 EasyExcel 文档和示例逐渐完善,但 Apache POI 作为一个老牌项目,有着更加丰富的文档和社区支持。
3 功能实现
3.1 异常处理及日志收集
涉及到文件的读写,最好是完整记录相关日志以及异常的处理,保证安全的同时,为代码运行提供流畅性。
3.1.1 自定义异常处理类
模块涉及到文件的读写,为了保证系统运行的流畅性,要添加异常处理,捕捉系统中的一些异常信息,然后帮助更快的定位到读写中出问题的地方。主要涉及到需要自定义的异常为:
- 导入异常:自定义异常类
ExcelImportException
继承运行异常类,实现异常的抛出; - 导出异常:
自定义异常类ExcelExportException
继承运行异常类,实现异常的抛出。 - 全局异常处理:
GlobalExceptionHandler
捕获后端服务抛出的异常,然后将异常信息返回前段,并输出到日志中。
具体实现见下:
// 导出异常类
public class ExcelExportException extends RuntimeException {
public ExcelExportException(String message) {
super(message);
}
public ExcelExportException(String message, Throwable cause) {
super(message, cause);
}
}
// 导入异常类
public class ExcelImportException extends RuntimeException {
public ExcelImportException(String message) {
super(message);
}
public ExcelImportException(String message, Throwable cause) {
super(message, cause);
}
}
// 全局异常捕获
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 捕捉处理自定义的导出异常
*
* @param e
* @return
*/
public ResponseEntity<String> handleExcelExportException(ExcelExportException e) {
// 输出日志
logger.error("导出异常", e);
// 返回异常信息 code=500
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
public ResponseEntity<String> handleExcelImportException(ExcelImportException e){
logger.error("导入异常",e);
// 返回状态码 500
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 捕捉处理一些未知异常
*
* @param e
* @return
*/
public ResponseEntity<String> handelException(Exception e) {
// 输出日志
logger.error("异常", e);
// 返回异常信息 code=500
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
3.1.2 日志输出
日志输出采用的是SpringBoot默认的日志输出库是Logback
。首先需要定义日志输出模版,在resources文件目录下:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/application.log</file>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT" />
<appender-ref ref="FILE" />
</root>
</configuration>
同时在SpringBoot项目配置文件中配置日志相关的配置。主要配置以下两点:
-
日志输出的路径
-
日志的默认输出级别(info、warn、error)
logging: file: path: /log/ level: root: info
具体的使用方法,在需要输出日志的控制层中,初始化日志工厂,然后使用日志工厂输出日志:
private static final Logger logger = LoggerFactory.getLogger(AnimalsController.class); // 日志输出 logger.error("导出文件异常", e);
3.2 Excel文件与Mysql的导入导出
使用阿里的Excel文件读写工具,实现文件的读写,首先导入相关依赖,以及mybatisPlus依赖,进行数据库配置。Mybatis配置参考官方文件,对应的SpringBoot版本不同,配置方法不同。官网文件:https://baomidou.com/introduce/。
SpringBoot2.X版本参考博客:CSDN
因为文件与数据库的匹配中可能存在一些字段是无关字段,所以需要创建一个新的DTO实体类同时进行序列化,如下:
@Data
public class AnimalsDto implements Serializable {
@ExcelProperty(value = "名字", index = 1)
private String name;
@ExcelProperty(value = "种类", index = 2)
private String species;
@ExcelProperty(value = "年龄", index = 3)
private Integer age;
@ExcelProperty(value = "价格", index = 4)
private Double price;
@ExcelProperty(value = "描述", index = 5)
private String description;
}
3.2.1 文件的导出
导出文件的基本步骤如下:
-
配置文件中配置导出路径。
# 文件导出路径 export: file: path: D:\Code\JavaCode\DBExport\dbExportFile\animals.xlsx
接口调用 -> 查询数据库(MybatisPlus的service层)-> 封装的DBExportUtils工具导出查询结果 -> 返回是否导出成功消息
- 封装一个导出工具类,将查询的数据集合写入Excel文件中,工具类到出方法
exportToExcel()
采用的是static
静态的方法,直接由类名调用,无需实例化类。同时在文件导出时,使用tyr-catch
捕获异常,并抛出导出异常。
public class DBExportUtils {
/**
* 将宠物list导出为excel表
* 该操作只在内存进行处理,减少IO开销
*
* @param animalsList 宠物列表
* @return
*/
public static void exportToExcel(List<TAnimals> animalsList, String filePath) {
try (FileOutputStream outputStream = new FileOutputStream(new File(filePath))){
// 直接写入文件系统
EasyExcel.write(outputStream, TAnimals.class).sheet("宠物信息").doWrite(animalsList);
} catch (Exception e) {
throw new ExcelExportException(e.getMessage(), e);
}
}
}
-
工具类在控制层调用,采用mybatisPlus查询数据库,存入DTO类集合中,然后调用工具类方法,是实现导出,同时输出日志。
注意:此处的返回信息没用经过JSON格式化,只是简单的返回了一个信息。
// 注入文件导出路劲
@Value("${export.file.path}")
private String filePath;
@GetMapping("/export")
public ResponseEntity<String> exportAnimalsToExcel() {
try {
List<TAnimals> animals = animalsService.list();
DBExportUtils.exportToExcel(animals, filePath);
return new ResponseEntity<>("导出成功:" + filePath, HttpStatus.OK);
} catch (ExcelExportException e) {
logger.error("导出文件异常", e);
return new ResponseEntity<>("文件导出失败", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
3.2.2 文件的导入
文件导入的基本步骤如下:
-
通过控制层,接受前端传来的文件,通过
MultipartFile
接口来处理文件,转为流的方式读取文件内容。 -
通过实现
EasyExcel
接口ReadListener
,实现表内数据的读取,并将数据映射到DTO字段,在转到到实体类集合。ReadListener 是 EasyExcel 库中用于处理 Excel 数据读取的监听器接口。它提供了一种机制,可以在数据读取过程中对每一行数据进行处理,并在数据读取完成时执行一些操作。通过实现 ReadListener 接口,用户可以自定义数据读取的行为,例如将读取到的数据进行转换、验证、缓存或批量处理。
ReadListener 接口主要有两个方法:
-
invoke 方法:处理从 Excel 文件中读取到的一行数据。
-
doAfterAllAnalysed 方法:在所有数据都读取完毕后执行的操作。
-
-
通过MybatisPlus实现数据库中数据插入。
**强调:**处理大量数据的导入时,可以采用分批处理的方式配合多线程,实现大量数据的导入操作。通过定义一个 cacheAnimalsList
静态常量(所有实例化的类对象共享!!!),缓存要处理的一批数据,具体见代码:
/**
* <p>描述: 数据读取监听类 </p>
* <p>创建时间: 2024/6/18 21:30</p>
*/
@Component
public class DataListener implements ReadListener<AnimalsDto> {
// 缓存读取的数据
private final List<TAnimals> cacheAnimalsList = new ArrayList<>();
@Autowired
private TAnimalsService animalsService;
// 每度取一条数据进行的操作
@Override
public void invoke(AnimalsDto animalsDto, AnalysisContext context) {
if (animalsDto != null) {
TAnimals animals = new TAnimals();
animals.setName(animalsDto.getName());
animals.setAge(animalsDto.getAge());
animals.setPrice(animalsDto.getPrice());
animals.setSpecies(animalsDto.getSpecies());
animals.setDescription(animalsDto.getDescription());
cacheAnimalsList.add(animals);
// 大量文件的分批处理
if (cacheAnimalsList.size() >= 100) {
// 保存到数据库
animalsService.saveBatch(cacheAnimalsList);
// 清除缓存数据
cacheAnimalsList.clear();
}
} else {
throw new ExcelImportException("文件未空,未读取");
}
}
/**
* 数据读取完毕需要进行的操作
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 保存到数据库
animalsService.saveBatch(cacheAnimalsList);
cacheAnimalsList.clear();
}
}
// 控制层
@PostMapping("/upload")
public ResponseEntity<String> importAnimalsToDB(@RequestBody MultipartFile file) {
// 日志输出
logger.info("收到传来的文件:{}", file.getOriginalFilename());
if (file.isEmpty()) {
logger.warn("文件为空");
return new ResponseEntity<>("文件为空", HttpStatus.INTERNAL_SERVER_ERROR);
}
// 将文件转为流的方式,进行读取
try(InputStream inputStream = file.getInputStream()) {
// 读取文件
EasyExcel.read(inputStream, AnimalsDto.class, dataListener)
.sheet().doRead();
return new ResponseEntity<>("文件上传并导入成功", HttpStatus.OK);
} catch (IOException e) {
logger.error("读取文件错误", e);
throw new ExcelImportException("文件上传失败", e);
}
}
4 关于一些关键类的说明
4.1 MultipartFile接口
MultipartFile
是 Spring 框架中用于处理文件上传的类。它表示一个上传的文件并提供了多种方法来获取文件的内容和元数据。MultipartFile
接口定义在 org.springframework.web.multipart
包中。
以下是 MultipartFile
接口中一些常用的方法及其功能:
- **
String getName()
**返回表单中上传文件字段的名称。 - **
String getOriginalFilename()
**返回上传文件的原始文件名。 - **
String getContentType()
**返回文件的 MIME 类型,如果无法确定则返回null
。 - **
boolean isEmpty()
**检查文件是否为空,即文件大小是否为 0 字节。 - **
long getSize()
**返回文件的大小(字节数)。 - **
byte[] getBytes()
**返回文件的内容,作为字节数组。 - **
InputStream getInputStream()
**返回文件内容的输入流,用于读取文件内容。 - **
void transferTo(File dest)
**将接收到的文件传输到给定的目标文件。目标文件路径可以是绝对路径或相对路径。
一些额外的说明:
配置文件上传大小:需要在 Spring 配置中设置最大上传文件大小,例如在 application.properties
文件中添加:
spring.servlet.multipart.max-file-size=2MB
spring.servlet.multipart.max-request-size=2MB
处理大文件上传:对于大文件上传,可能需要进行流式处理而不是一次性将文件内容读取到内存中。
安全性:处理文件上传时需要注意安全性,如防止文件名注入攻击,验证上传文件的类型和大小等。