1. EasyExcel简介
Java解析,生成Excel比较有名的框架有Apache POI,jxl。但他们都存在一个严重的问题就是非常的耗内存,poi有一套SAX模式的API可以一定程度的解决一些内存溢出的问题,但POI还是有一些缺陷,比如07版本Excel解压缩以及解压后存储都是在内存中完成的,内存消耗很大。easyexcel重写了poi对07版本excel的解析,一个3M的excel用poi sax解析需要100M左右的内存,改用easyexcel可以降低到几M,并且再大的excel也不会出现内存溢出,03版本依赖POI的sax模式,再上层坐了模型转换的封装,让使用者更加简单方便。
2. EasyExcel常用注解
2.1 @ExcelProperty
用于Excel和实体类直接的匹配
名称 | 默认值 | 描述 |
---|---|---|
value | 空 | 用于匹配excel中的头,必须全匹配,如果有多行头,会匹配最后一行头 |
order | Integer.MAX_VALUE | 优先级高于value ,会根据order 的顺序来匹配实体和excel中数据的顺序 |
index | -1 | 优先级高于value 和order ,会根据index 直接指定到excel中具体的哪一列 |
converter | 自动选择 | 指定当前字段用什么转换器,默认会自动选择。读的情况下只要实现com.alibaba.excel.converters.Converter#convertToJavaData(com.alibaba.excel.converters.ReadConverterContext<?>) 方法即可 |
2.2 @ExcelIgnore
默认所有字段都会和excel去匹配,家里这个注解会忽略该字段。
2.3 @ExcelIgnoreUnannotated
默认不加@ExcelProperty的注解的都会参与读写,加了不会参与读写
2.4 @DateTimeFormat
日期转换,用String去接收Excel日期格式的数据会调用这个注解。
名称 | 默认值 | 描述 |
---|---|---|
value | 空 | 参照java.text.SimpleDateFormat 书写即可 |
use1904windowing | 自动选择 | excel中时间是存储1900年起的一个双精度浮点数,但是有时候默认开始日期是1904,所以设置这个值改成默认1904年开始 |
2.5 NumberFormat
数字转换,用String去接收Excel数字格式的数据会调用这个注解。
名称 | 默认值 | 描述 |
---|---|---|
value | 空 | 参照java.text.DecimalFormat 书写即可 |
roundingMode | RoundingMode.HALF_UP | 格式化的时候设置舍入模式 |
3. 引入EasyExcel的依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>
4. 实现导入导出的处理
- 导入导出对象的实体类
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
// 类上加注解 @ExcelIgnoreUnannotated,过滤属性没有@ExcelProperty注解的字段
@ExcelIgnoreUnannotated
public class User {
/**
* user id.
*/
@ExcelProperty("ID")
private Long id;
/**
* 姓名.
*/
@ExcelProperty("姓名")
private String userName;
/**
* 性别.
*/
@ExcelProperty("性别")
private String gender;
/**
* 地址.
*/
@ExcelProperty("地址")
private String address;
/**
* 邮箱.
*/
@ExcelProperty("邮箱")
private String email;
/**
* 手机号码.
*/
@ExcelProperty("手机号码")
private Long phoneNumber;
/**
* 描述.
*/
@ExcelIgnore
@ExcelProperty("描述")
private String description;
}
- 导入导出的Controller
@GetMapping("/export")
public void exportUserInfo(HttpServletResponse response) {
try {
response.reset();
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
String fileName = "导出用户信息列表";
// 注意:这里要加上filename*=utf-8'zh_cn'否则可能会导致导出文件名乱码
response.setHeader("Content-disposition",
"attachment;filename*=utf-8'zh_cn'" + fileName + System.currentTimeMillis() + ".xlsx");
userService.exportUserInfo(response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
@PostMapping("/import")
public void importUserInfo(@RequestParam(value = "file") MultipartFile file) {
try {
userService.importUserInfo(file.getInputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
- 导入导出的Service
public interface UserService {
/**
* 导出文件
*
* @param outputStream
*/
void exportUserInfo(ServletOutputStream outputStream);
/**
* 导入文件
*
* @param inputStream
*/
void importUserInfo(InputStream inputStream);
}
- 导入导出的业务逻辑代码
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Override
public void exportUserInfo(ServletOutputStream outputStream) {
// 第一种方式
ExcelWriter excelWriter = EasyExcelFactory.write(outputStream).build();
WriteSheet userSheet = EasyExcelFactory.writerSheet(0)
.head(User.class)
// 导出文件需不包含的列名
.excludeColumnFieldNames(Lists.newArrayList())
// 导出文件包含的列名
.includeColumnFieldNames(Lists.newArrayList())
.build();
excelWriter.write(this::getUserList, userSheet);
excelWriter.finish();
// 第二种方式
EasyExcelFactory.write(outputStream, User.class).sheet("userInfo").doWrite(this::getUserList);
}
private List<User> getUserList() {
return Collections.singletonList(User.builder()
.id(1L).userName("itender").gender("男").address("广东深圳").email("[email protected]")
.phoneNumber(13156777777L).description("hello world")
.build());
}
@Override
public void importUserInfo(InputStream inputStream) {
// 第一种方式
ExcelDataListener excelDataListener = new ExcelDataListener();
ExcelReader excelReader = EasyExcelFactory.read(inputStream).build();
ReadSheet userSheet = EasyExcelFactory.readSheet(0)
.head(User.class)
.registerReadListener(excelDataListener)
.build();
excelReader.read(userSheet);
// 第二种方式
EasyExcelFactory.read(inputStream, User.class, new ReadListener<User>() {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存的数据
*/
private final List<User> cachedDataList = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
@Override
public void invoke(User user, AnalysisContext analysisContext) {
cachedDataList.add(user);
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
cachedDataList.forEach(user -> log.info(user.toString()));
}
}).sheet().doRead();
// 拿到错误信息,返回前端
String errorMsg = excelDataListener.getErrorMsg();
}
}
- 监听器
@Slf4j
public class ExcelDataListener implements ReadListener<User> {
/**
* 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 100;
/**
* 缓存的数据
*/
private static final List<User> CACHED_DATA_LIST = ListUtils.newArrayListWithExpectedSize(BATCH_COUNT);
/**
* 错误信息
*/
@Getter
private String errorMsg;
/**
* 这个每一条数据解析都会来调用
*
* @param user
* @param analysisContext
*/
@Override
public void invoke(User user, AnalysisContext analysisContext) {
log.info("解析到一条数据:{}", JSONUtil.toJsonStr(user));
// TODO 校验导入数据是否合规
// 如果不合规
this.errorMsg = StrFormatter.format("导入数据第{}行校验不通过!", analysisContext.readRowHolder().getRowIndex());
CACHED_DATA_LIST.add(user);
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (CACHED_DATA_LIST.size() >= BATCH_COUNT) {
// TODO 保存数据到MySQL
// 存储完成置空list
CACHED_DATA_LIST.clear();
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
// TODO 保存数据到MySQL
log.info("所有数据解析完成!");
}
}
问题:
-
在很多场景下,Excel的列与实体类可能并不完全一致,这时就需要排除一些实体类的字段。
方式一:类上加注解 @ExcelIgnoreUnannotated,过滤属性没有@ExcelProperty注解的字段
方式二:指定字段加@ExcelIgnore注解
方式三:代码指定过滤字段,通过excludeColumnFiledNames方法:
-
防止导出文件名乱码:
response.setHeader("Content-disposition", "attachment;filename*=utf-8'zh_cn'" + fileName + System.currentTimeMillis() + ".xlsx");
-
可以自定义Listener监听器实现导入数据校验,避免一次性导入太多数据,最好数据分批入库。