封裝了一個(gè)Excel導(dǎo)入加校驗(yàn)的工具,同事們用了都說(shuō)好
最近太忙了,剛剛到家趕緊抽空趕一篇,不知道能不能幫到你。
最近在做Excel導(dǎo)入功能,產(chǎn)品要求對(duì)導(dǎo)入數(shù)據(jù)先進(jìn)行校驗(yàn)然后再入庫(kù)。于是簡(jiǎn)單封裝了一個(gè)工具,結(jié)果兄弟們用了都說(shuō)好,今天就把思路分享出來(lái)。
easyexcel 庫(kù)
我們都知道POI是Java操作Excel的基礎(chǔ)庫(kù)。為了通用性并沒(méi)有做定制,而且還有一些局限性。經(jīng)過(guò)一番調(diào)研決定采用二次封裝庫(kù)easyexcel來(lái)進(jìn)行業(yè)務(wù)開發(fā)。
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>easyexcel</artifactId>
- <version>${easyexcel.version}</version>
- </dependency>
easyexcel將讀取Excel的生命周期抽象為了幾個(gè)階段,方便我們?cè)诟鱾€(gè)階段注入你想要實(shí)現(xiàn)的邏輯。這幾個(gè)階段包含在ReadListener接口中
- public interface ReadListener<T> extends Listener {
- /**
- * 當(dāng)任何一個(gè)偵聽器執(zhí)行錯(cuò)誤報(bào)告時(shí),所有偵聽器都將接收此方法。 如果在此處引發(fā)異常,則整個(gè)讀取將終止。
- * 這里是處理讀取excel異常的
- *
- * @param exception
- * @param context
- * @throws Exception
- */
- void onException(Exception exception, AnalysisContext context) throws Exception;
- /**
- * 讀取每行excel表頭時(shí)會(huì)執(zhí)行此方法
- *
- * @param headMap
- * @param context
- */
- void invokeHead(Map<Integer, CellData> headMap, AnalysisContext context);
- /**
- * 讀取每行數(shù)據(jù)的時(shí)候回執(zhí)行此方法
- *
- * @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);
- /**
- * 在整個(gè)excel sheet解析完畢后執(zhí)行的邏輯。
- *
- * @param context
- */
- void doAfterAllAnalysed(AnalysisContext context);
- /**
- * 用來(lái)控制是否讀取下一行的策略
- *
- * @param context
- * @return
- */
- boolean hasNext(AnalysisContext context);
- }
其抽象實(shí)現(xiàn)AnalysisEventListener
在你了解一個(gè)框架的抽象接口后,盡量要去看一下它有沒(méi)有能滿足你需要的實(shí)現(xiàn)。
另外這里要多說(shuō)一點(diǎn),接口中的AnalysisContext包含了很多有用的上下文元信息,比如 當(dāng)前行、當(dāng)前的配置策略、excel整體結(jié)構(gòu)等信息,你可以在需要的時(shí)候調(diào)用這些信息。
JSR303校驗(yàn)
最開始自己寫了一個(gè)抽象的校驗(yàn)工具,最后發(fā)現(xiàn)每一個(gè)字段都要編寫其具體的校驗(yàn)邏輯,如果一個(gè)Excel的字段量爆炸,這對(duì)開發(fā)來(lái)說(shuō)就可能是噩夢(mèng)。這使我想到了業(yè)界已經(jīng)有的規(guī)范-JSR303校驗(yàn)規(guī)范,它將數(shù)據(jù)模型(Model)和校驗(yàn)(Validation)各自抽象,非常靈活,而且工作量明顯降低。我們只需要找到和esayexcel生命周期結(jié)合的地方就行了。我們只需要引入以下依賴就能在Spring Boot項(xiàng)目中集成JSR303校驗(yàn):
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-validation</artifactId>
- </dependency>
關(guān)于JSR303相關(guān)的教程可以查看我這一篇文章。
實(shí)現(xiàn)過(guò)程
我們可以在解析每個(gè)字段的時(shí)候校驗(yàn),這對(duì)應(yīng)ReadListener的invoke(T data, AnalysisContext context)方法,這種方式可以實(shí)現(xiàn)當(dāng)字段校驗(yàn)觸發(fā)約束時(shí)就停止excel解析的策略;另一種可以在Excel解析完畢后執(zhí)行校驗(yàn),對(duì)應(yīng)doAfterAllAnalysed(AnalysisContext context)。這里以第二種為例我們來(lái)實(shí)現(xiàn)一下。
我們?cè)诰帉懘a時(shí),盡量職責(zé)單一,一個(gè)類或者一個(gè)方法盡量只干一個(gè)事,這樣讓自己的代碼足夠清晰。
編寫校驗(yàn)處理類
這里我把解析和校驗(yàn)分開實(shí)現(xiàn),先編寫JSR303校驗(yàn)工具。這里假設(shè)已經(jīng)有了校驗(yàn)器javax.validation.Validator的實(shí)現(xiàn),稍后我會(huì)講這個(gè)實(shí)現(xiàn)從哪里注入。
- 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 校驗(yàn)工具
- *
- * @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;
- /**
- * 集合校驗(yàn)
- *
- * @param data 待校驗(yàn)的集合
- * @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;
- }
- /**
- * 這里是校驗(yàn)的根本方法
- *
- * @param index 本條數(shù)據(jù)所在的行號(hào)
- * @param data 待校驗(yàn)的某條數(shù)據(jù)
- * @return 對(duì)數(shù)據(jù)的校驗(yàn)異常進(jìn)行提示,如果有觸發(fā)校驗(yàn)規(guī)則的會(huì)封裝提示信息。
- */
- private String doValidate(int index, T data) {
- // 這里使用了JSR303的的校驗(yàn)器,同時(shí)使用了分組校驗(yàn),Excel為分組標(biāo)識(shí)
- Set<ConstraintViolation<T>> validate = validator.validate(data, Excel.class);
- return validate.size()>0 ? "第" + index +
- "行,觸發(fā)約束:" + validate.stream()
- .map(ConstraintViolation::getMessage)
- .collect(Collectors.joining(",")): "";
- }
- }
上面就是整個(gè)校驗(yàn)的邏輯,如果校驗(yàn)通過(guò)不提示任何信息,如果校驗(yàn)不通過(guò)把校驗(yàn)的約束信息封裝返回。這里的Validator是從哪里來(lái)的呢?當(dāng)Spring Boot集成了JSR303會(huì)有一個(gè)Validator實(shí)現(xiàn)被自動(dòng)注入Spring IoC,我們可以利用它。
實(shí)現(xiàn)AnalysisEventListener
這個(gè)完全是easyexcel的功能了,我們只需要實(shí)現(xiàn)最開始提到的Excel抽象解析監(jiān)聽器接口AnalysisEventListener,并將解析字段加入集合,等完全解析完畢后再進(jìn)行校驗(yàn)。這里如果校驗(yàn)不通過(guò)就會(huì)拋出攜帶校驗(yàn)信息的異常,異常經(jīng)過(guò)處理返回前端提示。
切記:AnalysisEventListener的實(shí)現(xiàn)不能注入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總條數(shù)閾值
- */
- private static final Integer MAX_SIZE = 10000;
- /**
- * 校驗(yàn)工具
- */
- private final ExcelValidator<T> excelValidator;
- /**
- * 如果校驗(yàn)通過(guò)消費(fèi)解析得到的excel數(shù)據(jù)
- */
- private final Consumer<Collection<T>> batchConsumer;
- /**
- * 解析數(shù)據(jù)的臨時(shí)存儲(chǔ)容器
- */
- private final List<T> list = new ArrayList<>();
- /**
- * Instantiates a new Jdbc event listener.
- *
- * @param excelValidator Excel校驗(yàn)工具
- * @param batchConsumer Excel解析結(jié)果批量消費(fèi)工具,可實(shí)現(xiàn)為寫入數(shù)據(jù)庫(kù)等消費(fèi)操作
- */
- 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) {
- // 如果沒(méi)有超過(guò)閾值就把解析的excel字段加入集合
- if (list.size() >= MAX_SIZE) {
- throw new ServiceException("單次上傳條數(shù)不得超過(guò):" + MAX_SIZE);
- }
- list.add(data);
- }
- @Override
- public void doAfterAllAnalysed(AnalysisContext context) {
- //全部解析完畢后 對(duì)集合進(jìn)行校驗(yàn)并消費(fèi)
- 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的文檔封裝成一個(gè)通用的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();
- }
- /**
- * 解析需要的元數(shù)據(jù)
- *
- * @param <T> the type parameter
- */
- @Data
- public static class Meta<T> {
- /**
- * excel 文件流
- */
- private InputStream excelStream;
- /**
- * excel頭的行號(hào),參考easyexcel的api和你的實(shí)際情況
- */
- private Integer headRowNumber;
- /**
- * 對(duì)應(yīng)excel封裝的數(shù)據(jù)類,需要參考easyexcel教程
- */
- private Class<T> domain;
- /**
- * 解析結(jié)果的消費(fèi)函數(shù)
- */
- private Consumer<Collection<T>> consumer;
- }
- }
我們把這個(gè)工具注入Spring IoC,方便我們使用。
- /**
- * Excel 讀取工具
- *
- * @param validator the validator
- * @return the excel reader
- */
- @Bean
- public ExcelReader excelReader(Validator validator) {
- return new ExcelReader(validator);
- }
編寫接口
這里Excel的數(shù)據(jù)類ExcelData就不贅述了,過(guò)于簡(jiǎn)單!去看esayexcel的文檔即可。編寫一個(gè)Spring MVC接口示例,沒(méi)錯(cuò)就是這么簡(jiǎn)單。
- @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);
- // 批量寫入數(shù)據(jù)庫(kù)的邏輯
- excelDataMeta.setConsumer(dataService::saveBatch);
- this.excelReader.read(excelDataMeta);
- return RestBody.ok();
- }
總結(jié)
今天演示了如何將easyexcel和JSR303結(jié)合起來(lái),其實(shí)原理很簡(jiǎn)單,你只需要找到兩個(gè)技術(shù)的結(jié)合點(diǎn),并把它們組合起來(lái)即可,你學(xué)到了嗎?
本文轉(zhuǎn)載自微信公眾號(hào)「碼農(nóng)小胖哥」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼農(nóng)小胖哥公眾號(hào)。