在SpringBoot项目中使用EasyExcel-入门

876 阅读5分钟

0-编辑历史

2023-10-27 14:02,

宜网购 需求池都满了,请加速处理

1-参考资料

easyexcel.opensource.alibaba.com/

2-前情概要

在最近的项目开发中遇到了从Excel文档中导入数据,从数据库中导出数据至Excel表格的需求,因此了解了一下在Spring应用程序中处理Excel表格的相关技术,最终选择阿里开源的EasyExcel库来实现。

这个项目的基础是使用Spring Boot开发的一个RESTful Web Services,使用Mybatis-Plus作为持久层框架,数据库使用H2内存数据库,使用Gradle作为构建工具。

3-导入依赖

    // https://mvnrepository.com/artifact/com.alibaba/easyexcel
    implementation group: 'com.alibaba', name: 'easyexcel', version: '3.3.2'

4-Excel表格内容

有名称、品牌、价格、介绍、评分5列

Excel表格.png

5-实体类

上述Excel表格对应实体类如下所示,体现了Java 对象中的字段与 Excel 表格中的列之间的映射关系

@Data
@TableName(value = "coffee")
@JsonIgnoreProperties(ignoreUnknown=true)
@ExcelIgnoreUnannotated
public class Coffee {

    @TableId(value = "ID", type = IdType.AUTO)
    private Integer id;

    @ExcelProperty(value = "名称")
    @TableField(value = "name")
    private String name;

    @ExcelProperty(value = "品牌")
    @TableField(value = "brand")
    private String brand;

    @ExcelProperty(value = "价格")
    @TableField(value = "price")
    private Double price;

    @ExcelProperty(value = "介绍")
    @TableField(value = "introduction")
    private String introduction;

    @ExcelProperty(value = "评分")
    @TableField(value = "rating")
    private Double rating;

}

@ExcelIgnoreUnannotated 注解是 EasyExcel 提供的一个注解,用于标记在读取Excel文件时是否忽略未标记 @ExcelProperty 注解的字段。当我们使用 EasyExcel 读取 Excel 文件时,如果某个字段没有标记 @ExcelProperty 注解,则默认情况下会抛出异常。如果我们希望忽略这些未标记注解的字段,可以在类上添加 @ExcelIgnoreUnannotated 注解。

@ExcelProperty 注解是 EasyExcel 提供的另一个注解,用于标记 Java 对象中的字段与 Excel 表格中的列之间的映射关系。通过 @ExcelProperty 注解,我们可以指定 Java 对象中的哪个字段对应 Excel 表格中的哪一列,以及如何将 Excel 中的数据转换为 Java 对象中的数据。

6-从Excel表格导入数据

6-1-读取Excel表格监听器

/**
 * 有个很重要的点 CoffeeExcelDataListener 不能被spring管理
 * 要每次读取excel都要new,然后里面用到spring可以构造方法传进去
 *
 * @author 
 * @version 1.0
 * @since 2023 /10/25 16:05
 */
@Slf4j
public class CoffeeExcelDataListener implements ReadListener<Coffee> {

    /**
     * 每隔5条存储数据库,实际使用中可以100条,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 100;

    /**
     * 缓存的数据
     */
    private List<Coffee> cachedCoffeeDataList = new ArrayList<>(BATCH_COUNT);

    /**
     * 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
     */
    private CoffeeService coffeeService;

    /**
     * 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
     *
     * @param coffeeService
     */
    public CoffeeExcelDataListener(CoffeeService coffeeService) {
        this.coffeeService = coffeeService;
    }

    /**
     * When analysis one row trigger invoke function.
     *
     * @param data    one row value. It is same as {@link AnalysisContext#readRowHolder()}
     * @param context analysis context
     */
    @Override
    public void invoke(Coffee data, AnalysisContext context) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            String coffeeString = objectMapper.writeValueAsString(data);
            log.info("解析到一条数据:{}", coffeeString);
        } catch (Exception e) {
            e.printStackTrace();
        }
        cachedCoffeeDataList.add(data);

        // 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
        if (cachedCoffeeDataList.size() >= BATCH_COUNT) {
            coffeeService.saveBatch(cachedCoffeeDataList);
            // 存储完成清理 list
            cachedCoffeeDataList = new ArrayList<>(BATCH_COUNT);
        }
    }

    /**
     * if have something to do after all analysis
     *
     * @param context
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        coffeeService.saveBatch(cachedCoffeeDataList);
        log.info("所有数据解析完成!");
    }
}

6-2-服务接口

public interface CoffeeService extends IService<Coffee> {
    List<Coffee> importFromExcel(MultipartFile multipartFile) throws IOException;
}

6-3-服务实现类代码

@Service
public class CoffeeServiceImpl extends ServiceImpl<CoffeeMapper, Coffee> implements CoffeeService {
    
    @Override
    public List<Coffee> importFromExcel(MultipartFile multipartFile) throws IOException {
        List<Coffee> coffeeList = new ArrayList<>();
        InputStream inputStream = multipartFile.getInputStream();
        try {
            ExcelReader excelReader = EasyExcel.read(inputStream, Coffee.class, new CoffeeExcelDataListener(this)).build();
            // 构建一个sheet 这里可以指定名字或者no
            ReadSheet readSheet = EasyExcel.readSheet(0).head(Coffee.class).build();
            // 读取一个sheet
            excelReader.read(readSheet);
        } catch (Exception e) {
            log.error("导入失败" , e);
        }
        return coffeeList;
    }
}

6-4-控制器接口

使用 @RequestPart 注解处理HTTP请求参数, @RequestPart 注解用于处理多部分请求中的文件或二进制数据。它通常与 multipart/form-data 类型的请求一起使用。

@PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) 的作用是指定了处理 POST 请求的方法,并要求请求的 Content-Typemultipart/form-data,以便正确解析和处理多部分表单数据

@RestController
@RequestMapping("/coffee")
public class CoffeeController {

    @Autowired
    private CoffeeService coffeeService;
    
    @PostMapping(value = "/import", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    @Operation(summary = "Import coffees from Excel")
    public ResponseEntity<List<Coffee>> importCoffeeExcelFile(@RequestPart(value = "file") MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            return ResponseEntity.badRequest()
                    .body(Collections.emptyList());
        }
        try {
            List<Coffee> coffeeList = coffeeService.importFromExcel(file);
            return ResponseEntity.ok(coffeeList);
        } catch (IOException e) {
            return ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(Collections.emptyList());
        }
    }
}

7-导出数据至Excel表格

7-1-服务接口

public interface CoffeeService extends IService<Coffee> {
    void exportToExcel(HttpServletResponse httpServletResponse) throws IOException;
}

7-2-服务实现类接口

@Service
public class CoffeeServiceImpl extends ServiceImpl<CoffeeMapper, Coffee> implements CoffeeService {

    @Override
    public void exportToExcel(HttpServletResponse httpServletResponse) throws IOException {
        List<Coffee> coffeeList = this.list();
        // 响应文件名称
        String fileName = "coffee.xlsx";
        httpServletResponse.reset();
        httpServletResponse.setContentType("application/vnd.ms-excel");
        httpServletResponse.setCharacterEncoding("utf-8");
        // UTF-8
        httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
        // 设置响应头
        httpServletResponse.setHeader("Content-disposition", "attachment;filename=" + fileName);
        try {
            OutputStream outputStream = httpServletResponse.getOutputStream();
            ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
            WriteSheet writeSheet = EasyExcel.writerSheet(0, "Sheet1").head(Coffee.class).build();
            excelWriter.write(coffeeList, writeSheet);
            excelWriter.finish();
        } catch (Exception e) {
            log.error("导出失败" , e);
        }
    }
}

HttpServletResponse 的作用是处理 HTTP 响应。它提供了一组方法,使开发人员能够设置响应的状态码、响应头、响应体等内容。通过HttpServletResponse,开发人员可以向客户端发送数据、文件、重定向请求等,并控制响应的行为和格式。它在 Web 开发中起到了关键的作用,用于构建和发送 HTTP 响应给客户端。

7-3-控制器接口

@RestController
@RequestMapping("/coffee")
public class CoffeeController {

    @Autowired
    private CoffeeService coffeeService;

    @GetMapping("/export")
    @Operation(summary = "Export coffees to Excel")
    public void exportCoffeeExcelFile(HttpServletResponse httpServletResponse) throws Exception {
        coffeeService.exportToExcel(httpServletResponse);
    }
}

8-处理Excel工具库介绍

Apache POI:Apache POI是一个流行的Java库,用于处理Microsoft Office格式的文件,包括Excel。它提供了丰富的API,可以读取、写入和操作Excel文件。你可以使用poi和poi-ooxml模块来处理不同版本的Excel文件。

EasyExcel:EasyExcel是阿里巴巴开源的一个简单易用的Java库,专注于Excel的导入和导出。它提供了简洁的API,支持大量数据的快速导入和导出,并具有较低的内存占用。EasyExcel还支持注解方式配置导入导出的映射关系。

JExcelApi:JExcelApi是另一个流行的Java库,用于读取、写入和操作Excel文件。它提供了一组简单的API,使得处理Excel变得更加容易。然而,JExcelApi的开发已经停止,并且对于新版本的Excel文件支持不完整。

9-总结

这篇文章在一个Spring RESTful WebService的基础上进一步开发,添加了处理Excel的功能,使用EasyExcel库实现导入Excel和导出Excel。当然,这都是最简单的,作为个人学习记录。后续还可以在此基础上做深入开发。