早期我们实现excel导入导出的技术方案,可能会不假思索的选用Apache poi、jxl。但他们存在内存消耗大,编码相对繁琐。好在现在阿里开源了EasyExcel,看它的名字大概就可以猜出来,这框架的一个特点就是容易使用。其次easyExcel省内存。下图是使用easyexcel消耗的内存图
下边就带大家了解一下easyexcel
快速、简单避免OOM的java处理Excel工具
ps:其底层仍然是基于poi,不过做了优化
https://alibaba-easyexcel.github.io/
https://alibaba-easyexcel.github.io/quickstart/api.html
https://github.com/alibaba/easyexcel/tree/master/src/test/java/com/alibaba/easyexcel/test/demo
https://alibaba-easyexcel.github.io/quickstart/faq.html
不得不说阿里在开源这方面确实做得很好,文档啥的一应齐全。大家只要按上方介绍的进行操作,基本上就可以很好的使用easyExcel了。好像介绍到这边,这篇文章就可以结束了,不过既然我标题都写了一行代码实现导入导出,那我就来演示一下这个操作,对了,阿里目前的demo还没有提供导入字段校验,下边我也演示一下如何利用hibernate-validator与easyExcel集成实现字段校验
1、pom.xml
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.4</version>
</dependency>
2、编写实体
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ExcelDemoEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
@ExcelProperty(value="用户ID")
private Long userId;
/**
* 用户名
*/
@NotBlank(message="用户名不能为空",groups = ExcelGroup.class)
@ExcelProperty(value="用户名")
private String username;
/**
* 密码
*/
@NotBlank(message="密码不能为空", groups = AddGroup.class)
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@ExcelIgnore
private String password;
/**
* 邮箱
*/
@NotBlank(message="邮箱不能为空",groups = ExcelGroup.class)
@Email(message="邮箱格式不正确",groups = ExcelGroup.class)
@ExcelProperty(value = "邮箱")
private String email;
/**
* 手机号
*/
@ExcelProperty(value="手机号")
private String mobile;
/**
* 状态 0:禁用 1:正常
*/
@ExcelProperty(value="状态",converter = ExcelDemoEntityStatusConverter.class)
private Integer status;
/**
* 创建时间
*/
@ExcelProperty(value="创建时间")
private Date createTime;
/**
* 部门名称
*/
@NotNull(message="部门不能为空",groups = ExcelGroup.class)
@ExcelProperty(value="部门名称")
private String deptName;
}
2、实现ReadListener监听器
@Slf4j
public class BaseAnalysisEventListener<T> extends AnalysisEventListener<T> {
@Getter
private ExcelData<T> excelData;
private List<T> rows = new ArrayList<>();
private List<ErrorExcelRow> errorExcelRows = new ArrayList<>();
public BaseAnalysisEventListener() {
excelData = new ExcelData<>();
}
/**
* excel一行行解析
* @param entity
* @param analysisContext
*/
@Override
public void invoke(T entity, AnalysisContext analysisContext) {
boolean isPass = this.validatePass(entity,analysisContext);
if(isPass){
rows.add(entity);
}
}
/**
* excel数据行都解析完毕时调用
* @param analysisContext
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
excelData.setErrorRows(errorExcelRows);
excelData.setRows(rows);
}
/**
* 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则 继续读取下一行。
*
* @param exception
* @param analysisContext
* @throws Exception
*/
@Override
public void onException(Exception exception, AnalysisContext analysisContext) {
log.error("解析失败,但是继续解析下一行:{}", exception.getMessage());
ErrorExcelRow errorExcelRow = this.setAndReturnErrorExcelRow(analysisContext,exception.getMessage());
errorExcelRows.add(errorExcelRow);
// 如果是某一个单元格的转换异常 能获取到具体行号
// 如果要获取头的信息 配合invokeHeadMap使用
if (exception instanceof ExcelDataConvertException) {
ExcelDataConvertException excelDataConvertException = (ExcelDataConvertException)exception;
log.error("第{}行,第{}列解析异常,数据为:{}", excelDataConvertException.getRowIndex(),
excelDataConvertException.getColumnIndex(), excelDataConvertException.getCellData());
}
}
/**
* 这里为一行行的返回头
*
* @param headMap
* @param context
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
log.info("解析到一条头数据:{}", JSON.toJSONString(headMap));
}
private boolean validatePass(T entity, AnalysisContext analysisContext) {
ValidResult validResult = ValidatorUtils.allCheckValidate(entity,false, ExcelGroup.class);
if(!validResult.isSuccess()){
String errorMsg = StringUtils.join(validResult.getErrorMessages(),",");
ErrorExcelRow errorExcelRow = this.setAndReturnErrorExcelRow(analysisContext,errorMsg);
errorExcelRows.add(errorExcelRow);
}
return validResult.isSuccess();
}
private ErrorExcelRow setAndReturnErrorExcelRow(AnalysisContext analysisContext,String errorMsg){
int rowIndex = analysisContext.readRowHolder().getRowIndex();
int sheetNo = analysisContext.readSheetHolder().getSheetNo();
String sheetName = analysisContext.readSheetHolder().getSheetName();
ErrorExcelRow errorExcelRow = ErrorExcelRow.builder()
.rowNum(rowIndex).sheetName(sheetName).sheetNo(sheetNo)
.errorMessage(errorMsg).build();
return errorExcelRow;
}
}
ps:这段代码实现看起来和阿里的demo实现不大一样,主要是考虑要是每个entity读取每次都要搞这么一个监听器,太繁琐,我这边就实现了一个通用的监听器,只做数据校验和列表整合,把真正的业务逻辑实现外移到业务层去做
3、校验核心代码
public static <T> ValidResult allCheckValidate(T obj,boolean isShowField, Class<?>... groups) {
Set<ConstraintViolation<T>> constraintViolations = validator.validate(obj,groups);
return getValidResult(isShowField, constraintViolations);
}
private static <T> ValidResult getAllCheckValidResult(boolean isShowField,
Set<ConstraintViolation<T>> constraintViolations) {
//返回异常result
if (constraintViolations.size() > 0) {
List<String> errorMessages = new LinkedList<>();
Iterator<ConstraintViolation<T>> iterator = constraintViolations.iterator();
while (iterator.hasNext()) {
ConstraintViolation<T> violation = iterator.next();
if(isShowField){
errorMessages.add(String.format("%s:%s", violation.getPropertyPath().toString(), violation.getMessage()));
}else{
errorMessages.add(violation.getMessage());
}
}
return ValidResult.fail(errorMessages);
}
return ValidResult.success();
}
4、导入工具类核心方法
public <T> ExcelData<T> read(Class<T> entityClass) {
BaseAnalysisEventListener<T> listener = new BaseAnalysisEventListener<>();
EasyExcel.read(inputStream,entityClass,listener).sheet(sheetNo).headRowNumber(headRowNumber).doRead();
return listener.getExcelData();
}
public <T> ExcelData<T> readAll(Class<T> entityClass) {
BaseAnalysisEventListener<T> listener = new BaseAnalysisEventListener<>();
EasyExcel.read(inputStream,entityClass,listener).headRowNumber(headRowNumber).doReadAll();
return listener.getExcelData();
}
5、web层调用
ExcelData<ExcelDemoEntity> excelDemoEntityExcelData = ExcelReader.builder().inputStream(file.getInputStream()).headRowNumber(1).sheetNo(0).build().read(ExcelDemoEntity.class);
就一行代码就实现导入
6、效果图演示
利用postMan来演示
a、校验信息不通过
b、校验通过
导出其实比较简单,直接用easyExcel提供的write方法就搞定了,也是一行代码。不过我习惯性会浅浅的封装一层,其封装核心代码如下
public <T> void write(List<T> entityClassList, Class<T> entityClass) throws Exception{
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
fileName = URLEncoder.encode(fileName, "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(),entityClass).sheet(sheetNo).doWrite(entityClassList);
}
web层调用
ExcelWriter.builder().fileName("员工花名册").response(response).sheetNo(0).build().write(excelDemoEntities,ExcelDemoEntity.class);
除了获取业务集合,其真正实现导出的代码也是只有一行
效果图展示
上边的示例只是简单的演示一下easyexcel的功能,easyexcel提供的功能远不止上面的功能,比如实体字段和excel字段映射转换、根据自定义模板导出、多个工作表解析等等。这些功能阿里的demo都有相关示例,感兴趣的朋友可以查看如下链接
https://github.com/alibaba/easyexcel
进行学习了解。由于我项目使用的easyexcel版本,对一些复杂的数据读取,支持并不怎么友好。后边我又整合了另一个excel框架--easypoi,它可以支持比较复杂的excel读取,easypoi的相关教程,就不在本篇论述了。感兴趣的朋友可以查看如下链接
http://easypoi.mydoc.io/
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-easyexcel