我们在做项目的时候,Excel 导入与导出是项目中经常用到的功能,在 Java 中常用 poi
实现 Excel 的导入与导出。由于 poi 占用内存较大,在高并发下很容易发生 OOM 或者频繁 fullgc,阿里基于 poi 开源了 EasyExcel 项目。
除了节约内存,EasyExcel
还简化了 API,通过注解
映射 Excel 单元格与对象字段之间的关系,简单的几行代码就能搞定复杂的导入导出功能了。
首先需要引入依赖,坐标如下。
<dependency>
<groupId>com.zzuhkp</groupId>
<artifactId>easyexcel-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
不过很不幸的是目前还没传至中央仓库,需要的小伙伴可自行上传到私有仓库或直接把代码嵌入自己的项目。
为了接收 Excel 文件内容,我们需要定义一个对应的 Model 类。
@Data
public class DemoData {
@ExcelProperty(index = 0)
private Integer integer;
@ExcelProperty(index = 1)
private String string;
@ExcelProperty(index = 2)
private Date date;
}
然后使用 List
参数接收即可。
@PostMapping("/list/obj")
public List<DemoData> listObj(@ExcelParam List<DemoData> list) {
return list;
}
注意参数前添加了 @ExcelParam
注解,用来标识 Excel
文件参数。这样,一个导入功能实现了,是不是很简单呢?
默认情况下接收名称为 file
的表单字段作为 Excel 文件,如果不满足还可以修改。
@ExcelParam(value = "file", required = true)
有时候,我们可能比较关心对象对应 Excel 的元数据,例如这个对象是第几行记录产生的,这个对象的字段对应 Excel 第几列,这个时候我们可以使用 ReadRows
参数接收 Excel
。
@PostMapping("/list/rows")
public ReadRows<DemoData> readRows(@ExcelParam ReadRows<DemoData> readRows) {
return readRows;
}
ReadRows
使用两个字段记录行映射关系与列映射关系。
public class ReadRows<T> {
private ExcelReadHeadProperty excelReadHeadProperty;
private List<ReadRow<T>> rows;
}
ExcelReadHeadProperty
是 EasyExcel
自带的类,表示列映射关系的元数据。ReadRow
是框架自定义的类,表示行映射关系的元数据。
看下 ReadRow
定义吧。
public class ReadRow<T> {
// 行索引,从 0 开始
private final Integer rowIndex;
// 行记录对应对象
private final T data;
}
使用 ExcelReadHeadProperty
获取字段对应列索引的示例代码如下。
// 对象字段名称 -> 从 0 开始的列索引
Map<String, Integer> fieldColumnIndexMap = readRows.getExcelReadHeadProperty().getHeadMap().values()
.stream().collect(Collectors.toMap(Head::getFieldName, Head::getColumnIndex));
这里对 Excel 的导出进行了简单的支持。将 List
定义为 controller
方法返回值即可。
@ExcelResponse
@GetMapping("/list/download")
public List<DemoData> downloadList() {
return Arrays.asList(new DemoData(1, "hello", new Date()), new DemoData(2, "excel", new Date()));
}
需要注意的是使用 @ExcelResponse
注解表示响应内容为 Excel 文件。默认情况,下载的文件名称为 default.xlxs
,写入到名称为 Sheet1 的工作表中。如果不满足需求可以修改。
@ExcelResponse(fileName = "测试文件", sheetName = "工作表1")
参数校验是 Excel 导入常用的功能,这里进行了强有力的支持,使用体验如原生 spring boot 校验般顺滑。
开启校验
与 spring boot 原生使用方式一样,将 @Validated
或 @Valid
注解添加到 @ExcelParam
参数上即可。
@PostMapping("/list/obj")
public List<DemoData> listObj(@ExcelParam @Validated List<DemoData> list) {
return list;
}
校验规则定义
Bean Validation 定义校验规则
默认情况下框架使用 JSR-303 Bean Validation
规范定义的校验注解校验,需要手动引入 spring-boot-starter-validation
,可通过设置环境变量 easyexcel.validator.default.enable=false
关闭。
@Data
public class DemoData {
@NotNull(message = "参数不能为空")
private Integer integer;
private String string;
private Date date;
}
另外还可以自定义注解对对象校验。
... 省略其他元注解
@Constraint(validatedBy = {DemoDataValid.DemoDataValidator.class})
public @interface DemoDataValid {
... 省略注解属性
class DemoDataValidator implements ConstraintValidator<DemoDataValid, DemoData> {
@Override
public boolean isValid(DemoData value, ConstraintValidatorContext context) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate("测试对象校验").addConstraintViolation();
return false;
}
}
}
@DemoDataValid
public class DemoData {
... 省略属性
}
ExcelValidator 接口定义校验规则
Bean Validation
注解只能校验单个字段或对象,如果需要对所有的对象进行校验,可以实现框架定义的 ExcelValidator
接口,然后将实现定义为 Spring Bean。
这个接口定义如下。
public interface ExcelValidator<T> {
ExcelValidErrors validate(ReadRows<T> readRows);
}
ExcelValidErrors
用于接收校验的错误信息,分别使用接口 ExcelValidObjectError
和 ExcelValidFieldError
接口定义行错误信息和单元格错误信息。
public class ExcelValidErrors {
// 行错误信息或单元格错误信息列表
private final List<ExcelValidObjectError> errors;
}
public interface ExcelValidObjectError {
// 获取行号,从 1 开始
Integer getRow();
// 获取错误消息
String getMessage();
}
public interface ExcelValidFieldError extends ExcelValidObjectError {
// 获取列,从 1 开始
Integer getColumn();
}
例如,如果需要对所有的 DemoData 校验 integer 字段的值不能重复,可以使用如下的代码。
@Component
public class CustomExcelValidator implements ExcelValidator<DemoData> {
@Override
public ExcelValidErrors validate(ReadRows<DemoData> readRows) {
ExcelValidErrors errors = new ExcelValidErrors();
Map<Integer, List<ReadRow<DemoData>>> group = readRows.getRows().stream()
.collect(Collectors.groupingBy(item -> item.getData().getInteger()));
for (Map.Entry<Integer, List<ReadRow<DemoData>>> entry : group.entrySet()) {
if (entry.getValue().size() > 1) {
for (ReadRow<DemoData> readRow : entry.getValue()) {
errors.addError(new DefaultExcelObjectError(readRow.getRowIndex() + 1, "参数重复"));
}
}
}
return errors;
}
}
与 Spring MVC 设计类似,这里也提供了两种接收校验结果的方式。
异常捕获接收校验结果
开启校验后,如果校验结果中包含错误,会将错误信息封装到 ExcelValidException
,并抛出异常,可以通过全局异常捕获的方式收集错误信息。
@RestControllerAdvice
public class GlobalExceptionControllerAdvice {
@ExceptionHandler(ExcelValidException.class)
public String handleException(ExcelValidException e) {
ExcelValidErrors errors = e.getErrors();
return JSON.toJSONString(errors);
}
}
controller 方法参数接收校验结果
如果不想通过异常捕获的方式接收校验的错误信息,还可以将错误信息添加到 @ExcelParam 参数的后面,示例代码如下。
@PostMapping("/list/obj")
public List<DemoData> listObj(@ExcelParam @Validated List<DemoData> list, ExcelValidErrors errors) {
if (errors.hasErrors()) {
String messages = errors.getAllErrors().stream().map(ExcelValidObjectError::getMessage).collect(Collectors.joining(" | "));
throw new RuntimeException("发现异常:" + messages);
}
return list;
}