Springboot集成EasyExcel

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);
}

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,很高兴回答您的问题。SpringBoot集成EasyExcel可以大大简化Excel文件的读写操作。您可以按照以下步骤进行集成: 1. 添加EasyExcel依赖 在pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.6</version> </dependency> ``` 2. 添加EasyExcel配置 您可以添加以下配置来启用EasyExcel: ```java @Configuration public class EasyExcelConfig { @Bean public EasyExcelTemplate easyExcelTemplate() { // 设置读取文件的最大行数 EasyExcelTemplate easyExcelTemplate = new EasyExcelTemplate(); ReaderOption readerOption = new ReaderOption(); readerOption.setCount(10000); easyExcelTemplate.setReaderOption(readerOption); return easyExcelTemplate; } } ``` 3. 编写Excel文件读写代码 您可以按照以下代码编写读取和写入Excel文件的代码: ```java @Component public class ExcelService { @Autowired private EasyExcelTemplate easyExcelTemplate; /** * 写出数据到Excel文件 */ public void writeExcel(List<Object> data, String filePath) { ExcelWriter excelWriter = easyExcelTemplate.getWriter(filePath); WriteSheet writeSheet = EasyExcel.writerSheet(0).build(); // 设置表头和数据 excelWriter.write(data, writeSheet); // 输出文件 excelWriter.finish(); } /** * 读取Excel文件 */ public void readExcel(String filePath) { ExcelReader excelReader = easyExcelTemplate.getReader(filePath); List<Object> data = new ArrayList<>(); ReadSheet readSheet = EasyExcel.readerSheet(0).build(); excelReader.read(readSheet).forEach(object -> data.add(object)); // 输出数据 System.out.println(data); } } ``` 通过这样的方式,您就可以很方便地读写Excel文件了。希望能对您有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值