
最近太忙了,封装刚刚到家赶紧抽空赶一篇,个E工具不知道能不能帮到你。导入都说
最近在做Excel导入功能,加校产品要求对导入数据先进行校验然后再入库。同事于是封装简单封装了一个工具,结果兄弟们用了都说好,个E工具今天就把思路分享出来。导入都说
easyexcel 库
我们都知道POI是加校Java操作Excel的基础库。为了通用性并没有做定制,同事而且还有一些局限性。封装经过一番调研决定采用二次封装库easyexcel来进行业务开发。个E工具
<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>${ easyexcel.version}</version> </dependency> easyexcel将读取Excel的导入都说生命周期抽象为了几个阶段,方便我们在各个阶段注入你想要实现的加校逻辑。这几个阶段包含在ReadListener接口中
public interface ReadListener<T> extends Listener { /** * 当任何一个侦听器执行错误报告时,同事所有侦听器都将接收此方法。 如果在此处引发异常,则整个读取将终止。 * 这里是处理读取excel异常的 * * @param exception * @param context * @throws Exception */ void onException(Exception exception, AnalysisContext context) throws Exception; /** * 读取每行excel表头时会执行此方法 * * @param headMap * @param context */ void invokeHead(Map<Integer, CellData> headMap, AnalysisContext context); /** * 读取每行数据的时候回执行此方法 * * @param data * one row value. Is is same as { @link AnalysisContext#readRowHolder()} * @param context * analysis context */ void invoke(T data, AnalysisContext context); /** * 如果有额外的单元格信息返回就用此方法处理 * * @param extra * extra information * @param context * analysis context */ void extra(CellExtra extra, AnalysisContext context); /** * 在整个excel sheet解析完毕后执行的逻辑。 * * @param context */ void doAfterAllAnalysed(AnalysisContext context); /** * 用来控制是否读取下一行的策略 * * @param context * @return */ boolean hasNext(AnalysisContext context); } 其抽象实现AnalysisEventListener提供更加符合需要的抽象,我会进一步实现这个抽象来实现Excel的云南idc服务商导入和校验。
在你了解一个框架的抽象接口后,尽量要去看一下它有没有能满足你需要的实现。
另外这里要多说一点,接口中的AnalysisContext包含了很多有用的上下文元信息,比如 当前行、当前的配置策略、excel整体结构等信息,你可以在需要的时候调用这些信息。
JSR303校验
最开始自己写了一个抽象的校验工具,最后发现每一个字段都要编写其具体的校验逻辑,如果一个Excel的字段量爆炸,这对开发来说就可能是噩梦。这使我想到了业界已经有的规范-JSR303校验规范,它将数据模型(Model)和校验(Validation)各自抽象,非常灵活,而且工作量明显降低。我们只需要找到和esayexcel生命周期结合的地方就行了。我们只需要引入以下依赖就能在Spring Boot项目中集成JSR303校验:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> 关于JSR303相关的教程可以查看我这一篇文章。
实现过程
我们可以在解析每个字段的高防服务器时候校验,这对应ReadListener的invoke(T data, AnalysisContext context)方法,这种方式可以实现当字段校验触发约束时就停止excel解析的策略;另一种可以在Excel解析完毕后执行校验,对应doAfterAllAnalysed(AnalysisContext context)。这里以第二种为例我们来实现一下。
我们在编写代码时,尽量职责单一,一个类或者一个方法尽量只干一个事,这样让自己的代码足够清晰。
编写校验处理类
这里我把解析和校验分开实现,先编写JSR303校验工具。这里假设已经有了校验器javax.validation.Validator的实现,稍后我会讲这个实现从哪里注入。
import cn.felord.validate.Excel; import lombok.AllArgsConstructor; import org.springframework.util.StringUtils; import javax.validation.ConstraintViolation; import javax.validation.Validator; import java.util.*; import java.util.stream.Collectors; /** * excel 校验工具 * * @param <T> the type parameter * @author felord.cn * @since 2021 /4/14 14:14 */ @AllArgsConstructor public class ExcelValidator<T> { private final Validator validator; private final Integer beginIndex; /** * 集合校验 * * @param data 待校验的集合 * @return list */ public List<String> validate(Collection<T> data) { int index = beginIndex + 1; List<String> messages = new ArrayList<>(); for (T datum : data) { String validated = this.doValidate(index, datum); if (StringUtils.hasText(validated)) { messages.add(validated); } index++; } return messages; } /** * 这里是校验的根本方法 * * @param index 本条数据所在的行号 * @param data 待校验的某条数据 * @return 对数据的校验异常进行提示,如果有触发校验规则的会封装提示信息。 */ private String doValidate(int index, T data) { // 这里使用了JSR303的的校验器,同时使用了分组校验,Excel为分组标识 Set<ConstraintViolation<T>> validate = validator.validate(data, Excel.class); return validate.size()>0 ? "第" + index + "行,触发约束:" + validate.stream() .map(ConstraintViolation::getMessage) .collect(Collectors.joining(",")): ""; } } 上面就是整个校验的服务器租用逻辑,如果校验通过不提示任何信息,如果校验不通过把校验的约束信息封装返回。这里的Validator是从哪里来的呢?当Spring Boot集成了JSR303会有一个Validator实现被自动注入Spring IoC,我们可以利用它。
实现AnalysisEventListener
这个完全是easyexcel的功能了,我们只需要实现最开始提到的Excel抽象解析监听器接口AnalysisEventListener,并将解析字段加入集合,等完全解析完毕后再进行校验。这里如果校验不通过就会抛出携带校验信息的异常,异常经过处理返回前端提示。
切记:AnalysisEventListener的实现不能注入Spring IoC。
import cn.hutool.json.JSONUtil; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import cn.felord.exception.ServiceException; import org.springframework.util.CollectionUtils; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.function.Consumer; /** * 该类不可被Spring托管 * * @param <T> the type parameter * @author felord.cn * @since 2021 /4/14 14:19 */ public class JdbcEventListener<T> extends AnalysisEventListener<T> { /** * Excel总条数阈值 */ private static final Integer MAX_SIZE = 10000; /** * 校验工具 */ private final ExcelValidator<T> excelValidator; /** * 如果校验通过消费解析得到的excel数据 */ private final Consumer<Collection<T>> batchConsumer; /** * 解析数据的临时存储容器 */ private final List<T> list = new ArrayList<>(); /** * Instantiates a new Jdbc event listener. * * @param excelValidator Excel校验工具 * @param batchConsumer Excel解析结果批量消费工具,可实现为写入数据库等消费操作 */ public JdbcEventListener(ExcelValidator<T> excelValidator, Consumer<Collection<T>> batchConsumer) { this.excelValidator = excelValidator; this.batchConsumer = batchConsumer; } @Override public void onException(Exception exception, AnalysisContext context) throws Exception { list.clear(); throw exception; } @Override public void invoke(T data, AnalysisContext context) { // 如果没有超过阈值就把解析的excel字段加入集合 if (list.size() >= MAX_SIZE) { throw new ServiceException("单次上传条数不得超过:" + MAX_SIZE); } list.add(data); } @Override public void doAfterAllAnalysed(AnalysisContext context) { //全部解析完毕后 对集合进行校验并消费 if (!CollectionUtils.isEmpty(this.list)) { List<String> validated = this.excelValidator.validate(this.list); if (CollectionUtils.isEmpty(validated)) { this.batchConsumer.accept(this.list); } else { throw new ServiceException(JSONUtil.toJsonStr(validated)); } } } } 封装最终的工具
这里参考esayexcel的文档封装成一个通用的Excel读取工具
import com.alibaba.excel.EasyExcel; import lombok.AllArgsConstructor; import lombok.Data; import javax.validation.Validator; import java.io.InputStream; import java.util.Collection; import java.util.function.Consumer; /** * excel读取工具 * * @author felord.cn * @since 2021 /4/14 15:10 */ @AllArgsConstructor public class ExcelReader { private final Validator validator; /** * Read Excel. * * @param <T> the type parameter * @param meta the meta */ public <T> void read(Meta<T> meta) { ExcelValidator<T> excelValidator = new ExcelValidator<>(validator, meta.headRowNumber); JdbcEventListener<T> readListener = new JdbcEventListener<>(excelValidator, meta.consumer); EasyExcel.read(meta.excelStream, meta.domain, readListener) .headRowNumber(meta.headRowNumber) .sheet() .doRead(); } /** * 解析需要的元数据 * * @param <T> the type parameter */ @Data public static class Meta<T> { /** * excel 文件流 */ private InputStream excelStream; /** * excel头的行号,参考easyexcel的api和你的实际情况 */ private Integer headRowNumber; /** * 对应excel封装的数据类,需要参考easyexcel教程 */ private Class<T> domain; /** * 解析结果的消费函数 */ private Consumer<Collection<T>> consumer; } } 我们把这个工具注入Spring IoC,方便我们使用。
/** * Excel 读取工具 * * @param validator the validator * @return the excel reader */ @Bean public ExcelReader excelReader(Validator validator) { return new ExcelReader(validator); } 编写接口
这里Excel的数据类ExcelData就不赘述了,过于简单!去看esayexcel的文档即可。编写一个Spring MVC接口示例,没错就是这么简单。
@Autowired private ExcelReader excelReader; @Autowired private DataService dataService; @PostMapping("/excel/import") public Rest<?> importManufacturerInfo(@RequestPart MultipartFile file) throws IOException { InputStream inputStream = file.getInputStream(); ExcelReader.Meta<ExcelData> excelDataMeta = new ExcelReader.Meta<>(); excelDataMeta.setExcelStream(inputStream); excelDataMeta.setDomain(ExcelData.class); excelDataMeta.setHeadRowNumber(2); // 批量写入数据库的逻辑 excelDataMeta.setConsumer(dataService::saveBatch); this.excelReader.read(excelDataMeta); return RestBody.ok(); } 总结
今天演示了如何将easyexcel和JSR303结合起来,其实原理很简单,你只需要找到两个技术的结合点,并把它们组合起来即可,你学到了吗?
本文转载自微信公众号「码农小胖哥」,可以通过以下二维码关注。转载本文请联系码农小胖哥公众号。
