EasyExcel简介
Java领域解析生成Excel比较有名的框架有Apache poi、jxl等,这些框架都存在一个严重的问题:非常消耗内存。如果系统的并发量不大,可以使用这些框架,但是并发上来后,可能触发OOM或者JVM频繁的Full GC。
EasyExcel是阿里巴巴开源的一个Excel处理框架,以使用简单、节省内存著称。EasyExcel能大大减少占用内存的主要原因是在解析Excel时没有将文件数据一次性全部加载到内存中,而是从磁盘上一 行行读取数据,逐个解析。
EasyExcel采用一行一行的解析模式,并将一行的解析结果以观察者的模式通知处理 (AnalysisEventListener)。
官网地址:https://easyexcel.opensource.alibaba.com
EasyExcel相关依赖
<!-- EasyExcel依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.3</version>
</dependency>
<!-- mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.2.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
实体类
User实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
/**
* 主键
*/
@ExcelIgnore
private Integer id;
/**
* 用户名
*/
@ExcelProperty(value = "用户名")
@ColumnWidth(value = 20)
@NotBlank(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@ExcelProperty(value = "密码")
@ColumnWidth(value = 20)
@NotBlank(message = "密码不能为空")
private String password;
/**
* 邮箱
*/
@ExcelProperty(value = "邮箱")
@ColumnWidth(value = 30)
@Email(message = "邮箱格式错误")
private String email;
/**
* 电话
*/
@ExcelProperty(value = "电话")
@ColumnWidth(value = 20)
private String phone;
/**
* 找回密码问题
*/
@ExcelProperty(value = "找回密码问题")
@ColumnWidth(value = 50)
private String question;
/**
* 找回密码答案
*/
@ExcelProperty(value = "找回密码答案")
@ColumnWidth(value = 50)
private String answer;
/**
* 用户角色:0-管理员,1-普通用户
*/
@ExcelProperty(value = "用户角色", converter = RoleConverter.class)
@ColumnWidth(value = 20)
private Integer role;
/**
* 创建人
*/
@ExcelProperty(value = "创建人")
@ColumnWidth(value = 20)
private String createBy;
/**
* 创建时间
*/
@ExcelProperty(value = "创建时间")
@ColumnWidth(value = 30)
@DateTimeFormat(value = ExcelFormatConstants.LONG_DATE_FORMAT)
@NotNull(message = "创建时间不能为空")
private Date createTime;
/**
* 修改人
*/
@ExcelProperty(value = "修改人")
@ColumnWidth(value = 20)
private String updateBy;
/**
* 修改时间
*/
@ExcelProperty(value = "修改时间")
@ColumnWidth(value = 30)
@DateTimeFormat(value = ExcelFormatConstants.LONG_DATE_FORMAT)
private Date updateTime;
}
其中:
- @ExcelProperty:核心注解,value属性可用来设置表头名称,converter属性可以用来设置类型转换器。
- @ColumnWidth:用于设置表格列的宽度。
- @DateTimeFormat:用于设置日期转换格式。
- @NumberFormat:用于设置数字转换格式。
- @ExcelIgnore:导出时忽略该字段。
Excel格式常量定义
public class ExcelFormatConstants {
/**
* 日期格式
*/
public static final String DATE_FORMAT = "yyyy-MM-dd";
/**
* 长日期格式
*/
public static final String LONG_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
/**
* BigDecimal默认格式(保留两位小数)
*/
public static final String NUMBER_FORMAT_DEFAULT = "#,##0.00";
/**
* BigDecimal格式(保留四位小数)
*/
public static final String NUMBER_FORMAT_FOUR_DECIMAL = "#,##0.0000";
/**
* 分隔符
*/
public static final String SEPARATOR = ",";
/**
* 规格
*/
public static final int SCALE = -1;
}
自定义转换器
角色枚举类
@Getter
@AllArgsConstructor
public enum RoleEnum {
ADMINISTRATOR(0, "管理员"),
ORDINARY(1, "普通用户"),
UNKNOWN(-1, "未知角色");
/**
* 角色
*/
private Integer role;
/**
* 角色描述
*/
private String description;
/**
* 根据角色获取角色枚举对象
* @param role 角色
* @return 角色枚举对象
*/
public static RoleEnum convert(Integer role){
return Stream.of(values())
.filter(bean->bean.role.equals(role))
.findAny()
.orElse(UNKNOWN);
}
/**
* 根据角色描述获取角色枚举对象
* @param description 角色描述
* @return 角色枚举对象
*/
public static RoleEnum convert(String description){
return Stream.of(values())
.filter(bean->bean.description.equals(description))
.findAny()
.orElse(UNKNOWN);
}
}
自定义转换器
public class RoleConverter implements Converter<Integer> {
/**
* 指定转换器支持的Java类型
* @return
*/
@Override
public Class<?> supportJavaTypeKey() {
return Integer.class;
}
/**
* 指定转换器支持的Excel数据类型
* @return
*/
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return CellDataTypeEnum.STRING;
}
/**
* 将Excel单元格中的数据转换为Java对象
* @param context Excel单元格中的数据(如:角色描述)
* @return
*/
@Override
public Integer convertToJavaData(ReadConverterContext<?> context) {
return RoleEnum.convert(context.getReadCellData().getStringValue()).getRole();
}
/**
* 将Java对象中的数据转换为Excel单元格数据
* @param context Java对象中的数据(如:角色)
* @return
*/
@Override
public WriteCellData<?> convertToExcelData(WriteConverterContext<Integer> context) {
return new WriteCellData<>(RoleEnum.convert(context.getValue()).getDescription());
}
}
导入/导出工具类
Excel行导入结果
@Data
@Builder
public class ExcelLineResult<T> {
/**
* 行号(从0开始)
*/
private Integer rowIndex;
/**
* 导入的数据
*/
private T target;
/**
* 校验结果
*/
private Set<ConstraintViolation<T>> violation;
/**
* 业务异常错误信息
*/
private String bizError;
}
Excel错误信息填充器
@Slf4j
@RequiredArgsConstructor
public class ExcelErrorFillHandler<T> implements SheetWriteHandler, RowWriteHandler {
/**
* 错误结果集
*/
private final List<ExcelLineResult<T>> resultList;
/**
* 标题所在行(从1开始)
*/
private final Integer titleLineNumber;
/**
* 结果列序号
*/
private int resultColNum;
/**
* 默认导入成功的提示
*/
private static final String SUCCESS_MSG = "校验正常";
@Override
public void afterSheetCreate(SheetWriteHandlerContext context) {
SheetWriteHandler.super.afterSheetCreate(context);
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Sheet cachedSheet = writeSheetHolder.getCachedSheet();
for (int i = 1; i <= cachedSheet.getLastRowNum() + 1; i++) {
// 空白数据, 不做处理
if (i < titleLineNumber) {
continue;
}
Row row = cachedSheet.getRow(i - 1);
// 标题行,(创建标题)
if (i == titleLineNumber) {
// 获取标题行的列号
this.resultColNum = row.getLastCellNum();
// 在标题行创建新列
Cell cell = row.createCell(row.getLastCellNum(), CellType.STRING);
// 设置新列样式
setCellStyle(cell, IndexedColors.BLACK);
// 填充新列值
cell.setCellValue("校验结果");
continue;
}
// 数据行(填充校验信息)
// 在数据行创建新列
Cell cell = row.createCell(this.resultColNum, CellType.STRING);
// 获取数据行验证信息
String errMsg = convertErrMsg(resultList.get(i - titleLineNumber - 1));
// 如果数据行错误信息为空(即:校验正常),则填充校验正常信息
if (errMsg == null) {
// 设置新列样式
setCellStyle(cell, IndexedColors.GREEN);
// 填充新列值(即:校验正常)
cell.setCellValue(SUCCESS_MSG);
continue;
}
// 如果数据行错误信息不为空(即:校验异常),则填充校验异常信息
setCellStyle(cell, IndexedColors.RED);
cell.setCellValue("校验异常:" + errMsg);
}
}
private static void setCellStyle(Cell cell, IndexedColors color) {
// 获取工作簿
Workbook workbook = cell.getSheet().getWorkbook();
// 设置工作簿样式
CellStyle style = workbook.createCellStyle();
// 设置工作簿字体颜色
Font font = workbook.createFont();
font.setColor(color.getIndex());
style.setFont(font);
cell.setCellStyle(style);
}
/**
* 解析每行的错误信息
* @param result 读取结果
* @return 错误信息
*/
private String convertErrMsg(ExcelLineResult<T> result) {
if (result.getBizError() != null) {
return result.getBizError();
}
if (result.getViolation().isEmpty()) {
return null;
}
return result.getViolation().stream().map(ValidationUtil::getMessage)
.collect(Collectors.joining(";\n"));
}
}
Excel导入监听器
@Slf4j
@RequiredArgsConstructor
public class ExcelImportListener<T> implements ReadListener<T> {
private final List<ExcelLineResult<T>> excelLineResultList = new ArrayList<>();
public static String defaultBizError = "未知异常";
/**
* 业务处理/入库/解析等
*/
private final Consumer<T> consumer;
/**
* 每次读取, 记录读取信息
*/
@Override
public void invoke(T t, AnalysisContext analysisContext) {
if (log.isDebugEnabled()) {
log.debug("读取到第{}行数据: {}", analysisContext.readRowHolder().getRowIndex(), t);
}
ExcelLineResult<T> build = ExcelLineResult.<T>builder()
.rowIndex(analysisContext.readRowHolder().getRowIndex())
.target(t)
.build();
excelLineResultList.add(build);
}
/**
* 读取完毕后执行校验
*/
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
if (excelLineResultList.isEmpty()) {
return;
}
Validator validator = SpringContextUtil.getBean(Validator.class);
excelLineResultList.forEach(it -> {
// 校验行数据并返回校验结果集
Set<ConstraintViolation<T>> validate = validator.validate(it.getTarget());
// 向ExcelLineResult中设置校验结果集
it.setViolation(validate);
// 校验不通过, 不必执行业务逻辑
if (!validate.isEmpty()) {
return;
}
try {
consumer.accept(it.getTarget());
} catch (RuntimeException e) { // 自定义异常
log.error("解析数据失败: {}, 异常信息: {}", it, e.getMessage());
it.setBizError(e.getMessage());
} catch (Exception e) {
log.error("解析数据失败", e);
it.setBizError(defaultBizError);
}
});
}
public List<ExcelLineResult<T>> getExcelLineResultList() {
return excelLineResultList;
}
}
Excel工具类
@Slf4j
public class ExcelUtil {
private static final String SUFFIX = ".xlsx";
/**
* 导出到Excel, 并自动完成后续下载操作
* @param filename 文件名称, 不必填写后缀
* @param sheetName sheet页名称
* @param pojoClass 对应java类
* @param dataset 数据集
*/
public static void write(String filename, String sheetName, Class<?> pojoClass, Collection<?> dataset) {
// 获取当前响应对象
HttpServletResponse response = RequestContextUtil.getResponse();
try {
// 设置当前响应对象信息
setExcelResponse(response, filename);
// 导出数据文件
EasyExcel.write(response.getOutputStream(), pojoClass)
.sheet(sheetName)
.doWrite(dataset);
} catch (IOException e) {
throw new RuntimeException("文件导出失败", e);
}
}
/**
* 设置响应对象
* @param response 响应对象
* @param filename 文件名
* @throws UnsupportedEncodingException 编码异常
*/
public static void setExcelResponse(HttpServletResponse response, String filename) throws UnsupportedEncodingException {
if (!filename.endsWith(SUFFIX)) {
filename += SUFFIX;
}
// 清除缓冲区中的内容
response.reset();
// 设置ContentType
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("utf-8");
filename = URLEncoder.encode(filename, "utf-8").replace("\\+", "%20");
response.setHeader("filename", filename);
response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename);
}
/**
* 导入, 标题行默认为1
*
* @param file 文件
* @param pojoClass 实体类
* @param consumer 消费数据, 执行SQL逻辑或其他逻辑等等,
* 如果抛出RuntimeException异常(可以为自定义异常), 则异常message将作为Excel导入失败原因
* 否则为未知异常导致导入失败
* @param <T> 对应类型
*/
public static <T> List<ExcelLineResult<T>> read(@NotNull MultipartFile file, @NotNull Class<T> pojoClass, @NotNull Consumer<T> consumer) {
return read(file, pojoClass, consumer, 1);
}
/**
* 导入
*
* @param <T> 对应类型
* @param file 文件
* @param pojoClass 实体类
* @param consumer 消费数据, 执行SQL逻辑或其他逻辑等等,
* 如果抛出RuntimeException异常(可以为自定义异常), 则异常message将作为Excel导入失败原因
* 否则为未知异常导致导入失败
* @param titleLineNumber 标题所在行, 从1开始
* @return List<ExcelLineResult<T>>
*/
public static <T> List<ExcelLineResult<T>> read(@NotNull MultipartFile file,
@NotNull Class<T> pojoClass,
@NotNull Consumer<T> consumer,
@NotNull Integer titleLineNumber) {
try {
// 创建文件导入监听器
ExcelImportListener<T> listener = new ExcelImportListener<>(consumer);
// 获取导入文件流
@Cleanup InputStream inputStream = file.getInputStream();
// 读取导入文件信息
EasyExcel.read(inputStream, pojoClass, listener)
// 设置标题行(第1行)
.headRowNumber(titleLineNumber)
.sheet()
// 同步读取
.doReadSync();
// 获取导入文件行数据及校验结果
List<ExcelLineResult<T>> resultList = listener.getExcelLineResultList();
boolean allSuccess = resultList.stream()
.allMatch(it -> it.getViolation().isEmpty() && Objects.isNull(it.getBizError()));
// 如果导入文件所有行数据校验正常,则返回所有行数据(后续流程可对返回的所有行数据进行处理(如:插入数据库))
if (allSuccess) {
log.info("Excel数据校验正常: {}", resultList);
return resultList;
}
log.error("Excel数据校验异常, 校验结果: {}", resultList);
// 获取当前响应对象
HttpServletResponse response = RequestContextUtil.getResponse();
// 设置当前响应对象信息
setExcelResponse(response, "文件导入失败");
@Cleanup InputStream templateIs = file.getInputStream();
// 向导入失败文件中写入校验信息并导出导入失败文件
EasyExcel.write(response.getOutputStream(), pojoClass)
.withTemplate(templateIs)
.autoCloseStream(false)
.registerWriteHandler(new ExcelErrorFillHandler<T>(resultList, titleLineNumber))
.needHead(false)
.sheet()
.doWrite(Collections.emptyList());
} catch (Exception e) {
log.error("文件读取失败", e);
// 此处可换成自定义业务异常
throw new RuntimeException("文件读取失败, 请检查文件格式");
//throw new CustomException("文件读取失败, 请检查文件格式");
}
// 不做处理的异常, 某些场景不得不抛出异常, 以避免全局异常处理或全局响应处理来添加额外信息(此处可换成自定义业务异常)
throw new RuntimeException(null,null);
}
/**
* 下载Excel模版
* @param fileName 模版名称
* @param sheetName Sheet名称
* @param <T> 对应类型
*/
public static <T> void downLoadExcelTemplate(String fileName, String sheetName){
try {
// 获取当前响应对象
HttpServletResponse response = RequestContextUtil.getResponse();
// 设置当前响应对象信息
setExcelResponse(response, fileName);
// 生成Excel模版
EasyExcel.write(response.getOutputStream(), User.class).sheet(sheetName).doWrite(Collections.emptyList());
} catch (Exception e) {
log.error("下载Excel模版失败", e);
// 此处可换成自定义业务异常
throw new RuntimeException("下载Excel模版失败");
}
}
}
Spring上下文相关工具类
@Slf4j
public class RequestContextUtil {
/**
* @return 获取当前请求
*/
public static HttpServletRequest getRequest() {
return getRequestAttributes().getRequest();
}
/**
* @return 获取当前响应
*/
public static HttpServletResponse getResponse() {
return getRequestAttributes().getResponse();
}
private static ServletRequestAttributes getRequestAttributes() {
RequestAttributes attributes = Optional.ofNullable(RequestContextHolder.getRequestAttributes()).orElseThrow(() -> {
log.error("非web上下文无法获取请求属性, 异步操作请在同步操作内获取所需信息");
return new RuntimeException("请求异常");
});
return ((ServletRequestAttributes) attributes);
}
}
校验工具类
public class ValidationUtil {
public static String getMessage(ConstraintViolation<?> constraintViolation) {
String message = constraintViolation.getMessage();
if (!message.contains("{fieldTitle}")) {
return message;
}
String fieldTitle = "";
Class<?> rootBeanClass = constraintViolation.getRootBeanClass();
if (Objects.nonNull(rootBeanClass)) {
Field field = FieldUtils
.getField(rootBeanClass, constraintViolation.getPropertyPath().toString(), true);
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
if (Objects.nonNull(excelProperty) && excelProperty.value().length != 0) {
fieldTitle = excelProperty.value()[0];
}
}
return message.replace("{fieldTitle}", fieldTitle);
}
}
Spring工具类
@Component
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
/**
* 根据Class对象获取Bean
* @param clazz Class对象
* @param <T> 范型
* @return Bean
*/
public static <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
/**
* 根据Class对象及BeanName获取Bean
* @param name BeanName
* @param clazz Class对象
* @param <T> 范型
* @return Bean
*/
public static <T> T getBean(String name, Class<T> clazz) {
return context.getBean(name, clazz);
}
/**
* 获取ApplicationName
* @return ApplicationName
*/
public static String getId() {
return context.getId();
}
}
通过MapStruct定义集合映射
@Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
/**
* UserInfo集合转换为User集合
* @param source UserInfo集合
* @return User集合
*/
List<User> userInfoListInfoToUserList(List<UserInfo> source);
/**
* User集合转换为UserInfo集合
* @param source User集合
* @return User集合
*/
List<UserInfo> userListInfoToUserInfoList(List<User> source);
}
下载Excel模版
Excel模版下载接口
/**
* 用户信息Excel模版下载
*/
@GetMapping("/user/downLoadUserExcelTemplate")
public void downLoadUserExcelTemplate() {
ExcelUtil.downLoadExcelTemplate("用户列表模版", "用户列表");
}
导入Excel
Excel导入接口
@PostMapping("/user/import")
@ResponseBody
public String importUser(@RequestPart(value = "file") MultipartFile file) {
if(file == null){
return "导入文件为空";
}
List<ExcelLineResult<User>> lineResultList = ExcelUtil.read(file, User.class, user -> log.info("用户信息:{}", user));
if(!CollectionUtils.isEmpty(lineResultList)){
userService.saveBatch(UserMapper.INSTANCE.userListInfoToUserInfoList(lineResultList.stream().map(ExcelLineResult::getTarget).collect(Collectors.toList())));
}
return "导入成功";
}
导出Excel
Excel导出接口
/**
* 导出用户列表
*/
@GetMapping("/user/export")
public void export(){
// 获取用户列表
List<UserInfo> userInfos = userService.list();
List<User> users = UserMapper.INSTANCE.userInfoListInfoToUserList(userInfos);
ExcelUtil.write("用户列表-" + System.currentTimeMillis(), "用户列表",User.class, users);
}