Java Excel

easyexcel 库

POI是Java操作Excel的基础库。为了通用性并没有做定制,而且还有一些局限性。这里采用二次封装库easyexcel来进行业务开发。

  1. <dependency>
  2. <groupId>com.alibaba</groupId>
  3. <artifactId>easyexcel</artifactId>
  4. <version>${easyexcel.version}</version>
  5. </dependency>

easyexcel将读取Excel的生命周期抽象为了几个阶段,方便在各个阶段注入想要实现的逻辑。这几个阶段包含在ReadListener接口中

  1. public interface ReadListener<T> extends Listener {
  2. /**
  3. * 当任何一个侦听器执行错误报告时,所有侦听器都将接收此方法。 如果在此处引发异常,则整个读取将终止。
  4. * 这里是处理读取excel异常的
  5. *
  6. * @param exception
  7. * @param context
  8. * @throws Exception
  9. */
  10. void onException(Exception exception, AnalysisContext context) throws Exception;
  11. /**
  12. * 读取每行excel表头时会执行此方法
  13. *
  14. * @param headMap
  15. * @param context
  16. */
  17. void invokeHead(Map<Integer, CellData> headMap, AnalysisContext context);
  18. /**
  19. * 读取每行数据的时候回执行此方法
  20. *
  21. * @param data
  22. * one row value. Is is same as {@link AnalysisContext#readRowHolder()}
  23. * @param context
  24. * analysis context
  25. */
  26. void invoke(T data, AnalysisContext context);
  27. /**
  28. * 如果有额外的单元格信息返回就用此方法处理
  29. *
  30. * @param extra
  31. * extra information
  32. * @param context
  33. * analysis context
  34. */
  35. void extra(CellExtra extra, AnalysisContext context);
  36. /**
  37. * 在整个excel sheet解析完毕后执行的逻辑。
  38. *
  39. * @param context
  40. */
  41. void doAfterAllAnalysed(AnalysisContext context);
  42. /**
  43. * 用来控制是否读取下一行的策略
  44. *
  45. * @param context
  46. * @return
  47. */
  48. boolean hasNext(AnalysisContext context);
  49. }

其抽象实现AnalysisEventListener<T>提供更加符合需要的抽象,进一步实现这个抽象来实现Excel的导入和校验。
在了解一个框架的抽象接口后,尽量要去看一下它有没有能满足需要的实现。
另外这里要多说一点,接口中的AnalysisContext包含了很多有用的上下文元信息,比如当前行、当前的配置策略、excel整体结构等信息,可以在需要的时候调用这些信息。

JSR303校验

业界已经有的规范-JSR303校验规范,它将数据模型(Model)和校验(Validation)各自抽象,非常灵活,而且工作量明显降低。只需要找到和esayexcel生命周期结合的地方就行了。只需要引入以下依赖就能在Spring Boot项目中集成JSR303校验:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-validation</artifactId>
  4. </dependency>

实现过程

可以在解析每个字段的时候校验,这对应ReadListenerinvoke(T data, AnalysisContext context)方法,这种方式可以实现当字段校验触发约束时就停止excel解析的策略;另一种可以在Excel解析完毕后执行校验,对应doAfterAllAnalysed(AnalysisContext context)。这里以第二种为例来实现一下。
在编写代码时,尽量职责单一,一个类或者一个方法尽量只干一个事,这样让自己的代码足够清晰。

编写校验处理类

这里把解析和校验分开实现,先编写JSR303校验工具。这里假设已经有了校验器javax.validation.Validator的实现。

  1. import cn.fcant.validate.Excel;
  2. import lombok.AllArgsConstructor;
  3. import org.springframework.util.StringUtils;
  4. import javax.validation.ConstraintViolation;
  5. import javax.validation.Validator;
  6. import java.util.*;
  7. import java.util.stream.Collectors;
  8. /**
  9. * excel 校验工具
  10. *
  11. * @param <T> the type parameter
  12. */
  13. @AllArgsConstructor
  14. public class ExcelValidator<T> {
  15. private final Validator validator;
  16. private final Integer beginIndex;
  17. /**
  18. * 集合校验
  19. *
  20. * @param data 待校验的集合
  21. * @return list
  22. */
  23. public List<String> validate(Collection<T> data) {
  24. int index = beginIndex + 1;
  25. List<String> messages = new ArrayList<>();
  26. for (T datum : data) {
  27. String validated = this.doValidate(index, datum);
  28. if (StringUtils.hasText(validated)) {
  29. messages.add(validated);
  30. }
  31. index++;
  32. }
  33. return messages;
  34. }
  35. /**
  36. * 这里是校验的根本方法
  37. *
  38. * @param index 本条数据所在的行号
  39. * @param data 待校验的某条数据
  40. * @return 对数据的校验异常进行提示,如果有触发校验规则的会封装提示信息。
  41. */
  42. private String doValidate(int index, T data) {
  43. // 这里使用了JSR303的的校验器,同时使用了分组校验,Excel为分组标识
  44. Set<ConstraintViolation<T>> validate = validator.validate(data, Excel.class);
  45. return validate.size()>0 ? "第" + index +
  46. "行,触发约束:" + validate.stream()
  47. .map(ConstraintViolation::getMessage)
  48. .collect(Collectors.joining(",")): "";
  49. }
  50. }

上面就是整个校验的逻辑,如果校验通过不提示任何信息,如果校验不通过把校验的约束信息封装返回。这里的Validator是从哪里来的呢?当Spring Boot集成了JSR303会有一个Validator实现被自动注入Spring IoC,可以利用它来完成校验操作。

实现AnalysisEventListener

这个完全是easyexcel的功能了,只需要实现最开始提到的Excel抽象解析监听器接口AnalysisEventListener,并将解析字段加入集合,等完全解析完毕后再进行校验。这里如果校验不通过就会抛出携带校验信息的异常,异常经过处理返回前端提示。
切记:AnalysisEventListener的实现不能注入Spring IoC。

  1. import cn.hutool.json.JSONUtil;
  2. import com.alibaba.excel.context.AnalysisContext;
  3. import com.alibaba.excel.event.AnalysisEventListener;
  4. import cn.fcant.exception.ServiceException;
  5. import org.springframework.util.CollectionUtils;
  6. import java.util.ArrayList;
  7. import java.util.Collection;
  8. import java.util.List;
  9. import java.util.function.Consumer;
  10. /**
  11. * 该类不可被Spring托管
  12. *
  13. * @param <T> the type parameter
  14. */
  15. public class JdbcEventListener<T> extends AnalysisEventListener<T> {
  16. /**
  17. * Excel总条数阈值
  18. */
  19. private static final Integer MAX_SIZE = 10000;
  20. /**
  21. * 校验工具
  22. */
  23. private final ExcelValidator<T> excelValidator;
  24. /**
  25. * 如果校验通过消费解析得到的excel数据
  26. */
  27. private final Consumer<Collection<T>> batchConsumer;
  28. /**
  29. * 解析数据的临时存储容器
  30. */
  31. private final List<T> list = new ArrayList<>();
  32. /**
  33. * Instantiates a new Jdbc event listener.
  34. *
  35. * @param excelValidator Excel校验工具
  36. * @param batchConsumer Excel解析结果批量消费工具,可实现为写入数据库等消费操作
  37. */
  38. public JdbcEventListener(ExcelValidator<T> excelValidator, Consumer<Collection<T>> batchConsumer) {
  39. this.excelValidator = excelValidator;
  40. this.batchConsumer = batchConsumer;
  41. }
  42. @Override
  43. public void onException(Exception exception, AnalysisContext context) throws Exception {
  44. list.clear();
  45. throw exception;
  46. }
  47. @Override
  48. public void invoke(T data, AnalysisContext context) {
  49. // 如果没有超过阈值就把解析的excel字段加入集合
  50. if (list.size() >= MAX_SIZE) {
  51. throw new ServiceException("单次上传条数不得超过:" + MAX_SIZE);
  52. }
  53. list.add(data);
  54. }
  55. @Override
  56. public void doAfterAllAnalysed(AnalysisContext context) {
  57. //全部解析完毕后 对集合进行校验并消费
  58. if (!CollectionUtils.isEmpty(this.list)) {
  59. List<String> validated = this.excelValidator.validate(this.list);
  60. if (CollectionUtils.isEmpty(validated)) {
  61. this.batchConsumer.accept(this.list);
  62. } else {
  63. throw new ServiceException(JSONUtil.toJsonStr(validated));
  64. }
  65. }
  66. }
  67. }

封装最终的工具

这里参考esayexcel的文档封装成一个通用的Excel读取工具

  1. import com.alibaba.excel.EasyExcel;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import javax.validation.Validator;
  5. import java.io.InputStream;
  6. import java.util.Collection;
  7. import java.util.function.Consumer;
  8. /**
  9. * excel读取工具
  10. *
  11. */
  12. @AllArgsConstructor
  13. public class ExcelReader {
  14. private final Validator validator;
  15. /**
  16. * Read Excel.
  17. *
  18. * @param <T> the type parameter
  19. * @param meta the meta
  20. */
  21. public <T> void read(Meta<T> meta) {
  22. ExcelValidator<T> excelValidator = new ExcelValidator<>(validator, meta.headRowNumber);
  23. JdbcEventListener<T> readListener = new JdbcEventListener<>(excelValidator, meta.consumer);
  24. EasyExcel.read(meta.excelStream, meta.domain, readListener)
  25. .headRowNumber(meta.headRowNumber)
  26. .sheet()
  27. .doRead();
  28. }
  29. /**
  30. * 解析需要的元数据
  31. *
  32. * @param <T> the type parameter
  33. */
  34. @Data
  35. public static class Meta<T> {
  36. /**
  37. * excel 文件流
  38. */
  39. private InputStream excelStream;
  40. /**
  41. * excel头的行号,参考easyexcel的api和你的实际情况
  42. */
  43. private Integer headRowNumber;
  44. /**
  45. * 对应excel封装的数据类,需要参考easyexcel教程
  46. */
  47. private Class<T> domain;
  48. /**
  49. * 解析结果的消费函数
  50. */
  51. private Consumer<Collection<T>> consumer;
  52. }
  53. }

把这个工具注入Spring IoC,方便使用。

  1. /**
  2. * Excel 读取工具
  3. *
  4. * @param validator the validator
  5. * @return the excel reader
  6. */
  7. @Bean
  8. public ExcelReader excelReader(Validator validator) {
  9. return new ExcelReader(validator);
  10. }

编写接口

这里Excel的数据类ExcelData就不赘述了,过于简单!去看esayexcel的文档即可。编写一个Spring MVC接口示例。

  1. @Autowired
  2. private ExcelReader excelReader;
  3. @Autowired
  4. private DataService dataService;
  5. @PostMapping("/excel/import")
  6. public Rest<?> importManufacturerInfo(@RequestPart MultipartFile file) throws IOException {
  7. InputStream inputStream = file.getInputStream();
  8. ExcelReader.Meta<ExcelData> excelDataMeta = new ExcelReader.Meta<>();
  9. excelDataMeta.setExcelStream(inputStream);
  10. excelDataMeta.setDomain(ExcelData.class);
  11. excelDataMeta.setHeadRowNumber(2);
  12. // 批量写入数据库的逻辑
  13. excelDataMeta.setConsumer(dataService::saveBatch);
  14. this.excelReader.read(excelDataMeta);
  15. return RestBody.ok();
  16. }