sprngboot导入基于Easyexcel经典导入案例

sprngboot导入基于Easyexcel经典导入案例

一、框架环境介绍

1、springboot 2.2.6.RELEASE

2、easyexcel 3.1.1

二、demo编写流程

1、引入依赖包

        <dependency>
            <groupId>com.alibaba</groupId>
                <artifactId>easyexcel</artifactId>
            <version>3.1.1</version>
        </dependency>

2、相关编码

@RestController
@RequestMapping("consumerHandler")
public class ExcelController {

    @Autowired
    private ScoreImportService scoreImportService;

    @PostMapping("/import-scores")
    public ImportResDTO importScores(@RequestParam("file") MultipartFile file) throws IOException {
        EasyExcelListener listener = scoreImportService.importScores(file);
        return listener.getRes();
    }
}

此处接口层入参设计为:文本对象,每条栏目的对应解析实体,导入新的数据插入集合,导入更新时需要更新的数据集合。

package com.jiuzhou.desgin.excel;

import com.alibaba.excel.context.AnalysisContext;

import java.util.List;

/**
 * 接口
 * github地址 http://www.github.com/wanyushu
 * gitee地址 http://www.gitee.com/wanyushu
 *
 * @author yushu
 * @email 921784721@qq.com
 * @date 2025/1/7 11:13
 */
public interface ImportService<T,R> {
    /**
     * 处理数据接口
     * @param context
     * @param t
     * @param r
     * @param importList
     * @param updateList
     */
    String importCheck(AnalysisContext context, T t, R r, List<T> importList, List<T> updateList);

    /**
     * 返回元数据相关信息
     */

    String importCheck(AnalysisContext context, T t, R r, List<T> importList, List<T> updateList, List<T> sList);
    /**
     * 保存导入的信息
     * @param importList 导入的数据对象
     */
    void saveData( List<T> importList);

    /**
     * 需要更新的data
     */
    void updateData( List<T> importList);

    /**
     * 数据解析完成时
     */
    String uploadMeta(List<T> sList);
}

此类封装了通常导入所需要的基础方法,并将通用的导入需要用到的数据判空处理,数据是否符合预期要求进行了转换,并做了通常导入过程中最后一条全空的判断,如需求有要求导入的元数据如果异常,需要连同一起添加批注信息的,此处也进行了集成。

package com.jiuzhou.desgin.excel.Impl;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.fastjson.JSON;
import com.jiuzhou.desgin.excel.CommonImportService;
import com.jiuzhou.desgin.excel.dto.ImportResDTO;
import com.jiuzhou.desgin.excel.fun.CommentCellWriteHandler;
import com.jiuzhou.desgin.excel.vo.StudentScore;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;

/**
 * github地址 http://www.github.com/wanyushu
 * gitee地址 http://www.gitee.com/wanyushu
 * @author yushu
 * @email 921784721@qq.com
 * @date 2025/1/7 11:26
 */
@Slf4j
@Service
public class ScoreImportServiceImpl extends CommonImportService<StudentScore>  {



    @Override
    public String importCheck(AnalysisContext context, StudentScore studentScore,
                              ImportResDTO importResDTO, List<StudentScore> importList,
                              List<StudentScore> updateList, List<StudentScore> sList) {
        String s = this.importCheck(context, studentScore, importResDTO, importList, updateList);
        studentScore.setComment(s);
        sList.add(studentScore);
        return s;
    }

    @Override
    public String importCheck(AnalysisContext context, StudentScore studentScore, ImportResDTO importResDTO,
                            List<StudentScore> importList, List<StudentScore> updateList) {
        int row = context.getCurrentRowNum()+1;
        //检查是否都是空
        if(this.checkLastLine(studentScore)){
            return null;
        }
        String s = this.checkData(studentScore);

        importResDTO.setTotal(importResDTO.getTotal()+1);
        if(StringUtils.isEmpty(s)){
            //todo 判断数据是否存在
            boolean exit= false;
            if(exit){
                updateList.add(studentScore);
            }else{
                importList.add(studentScore);
            }
            importResDTO.setSucc(importResDTO.getSucc()+1);
        }else{
            importResDTO.getErrorMap().put("第"+row+"行",s);
            int fail = importResDTO.getFail();
            importResDTO.setFail(fail+1);
        }
        return s;
    }

    @Override
    public void saveData(List<StudentScore> importList) {
        log.info("开始新增数据:{}", JSON.toJSONString(importList));
        //todo 实际入库操作
    }

    @Override
    public void updateData(List<StudentScore> importList) {
        log.info("开始更新数据:{}", JSON.toJSONString(importList));
        //todo 实际入库操作
    }

    @Override
    public String uploadMeta(List<StudentScore> sList) {
        log.info("开始上传元数据相关信息到文件服务器以便后续下载异常信息");
        try {
            // 重新写入文件并添加批注样式
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            EasyExcel.write(outputStream,StudentScore.class)
                    .registerWriteHandler(getCellStyleStrategy())
                    .registerWriteHandler(new CommentCellWriteHandler())
                    .sheet("data")
                    .doWrite(sList);
            InputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
            // todo 将文件存入文件服务器

            String replace = UUID.randomUUID().toString().replace("-", "");
            return replace;
        } catch (Exception e) {
            log.error("异常文件上传至服务器发生异常");
            e.printStackTrace();
        }
        return null;
    }

}

此处将保存和更新数据的方法进行了抽象并集成通用检查方法。


package com.jiuzhou.desgin.excel;

import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.context.CellWriteHandlerContext;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.jiuzhou.desgin.excel.dto.ImportResDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;

import java.util.List;

/**
 * github地址 http://www.github.com/wanyushu
 * gitee地址 http://www.gitee.com/wanyushu
 * @author yushu
 * @email 921784721@qq.com
 * @date 2025/1/7 11:30
 */
@Slf4j
public  abstract class CommonImportService<T > extends BaseCommon implements ImportService<T, ImportResDTO> {

    public abstract void saveData(List<T> importList);

    public abstract void updateData(List<T> importList);

    public String uploadMeta(List<T> sList){
        return null;
    }

    protected HorizontalCellStyleStrategy getCellStyleStrategy() {
        // 表头样式
        WriteCellStyle headCellStyle = new WriteCellStyle();
        WriteFont headFont = new WriteFont();
        headFont.setFontHeightInPoints((short) 12);
        headCellStyle.setWriteFont(headFont);

        // 内容样式
        WriteCellStyle contentCellStyle = new WriteCellStyle();
        contentCellStyle.setWrapped(true);

        return new HorizontalCellStyleStrategy(headCellStyle, contentCellStyle);
    }

}

该类通过在导入的实体对象上面加注解,判断对应的栏目的取值是否为空,如果不是空则判断,该值有没有需要查询的字典值,如果符合预期则,替换为数据字典的value

package com.jiuzhou.desgin.excel;

import com.jiuzhou.desgin.excel.annotation.CheckData;
import org.apache.commons.lang.StringUtils;

import java.lang.reflect.Field;

/**
 * github地址 http://www.github.com/wanyushu
 * gitee地址 http://www.gitee.com/wanyushu
 *
 * @author yushu
 * @email 921784721@qq.com
 * @date 2025/1/7 15:09
 */
public class BaseCommon<T> {

    protected static final String NO_EMPTY= "不能为空,";

    public String checkData(T t){
        String info = null;
        StringBuilder sb = new StringBuilder();
        for (Field field : t.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(CheckData.class)) {
                CheckData apiModelProperty = field.getAnnotation(CheckData.class);
                if (apiModelProperty.required()) { // 判断是否必填
                    field.setAccessible(true); // 允许访问私有属性
                    try {
                        Object value = field.get(t);
                        if (value == null) {
                            // 使用 value 属性描述字段名称,添加错误信息到 importResDTO
                            sb.append(apiModelProperty.value());
                            sb.append(NO_EMPTY);
                            info=sb.toString();
                            //值是否符合字典要求
                        }else if(StringUtils.isNotBlank(apiModelProperty.dictName())){

                            sb = this.searchDictList(sb,apiModelProperty.dictName(),value,apiModelProperty.value(),field,t);
                            info = sb.toString();
                        }
                    } catch (IllegalAccessException e) {
                        e.printStackTrace(); // 处理异常
                    }
                }else if(StringUtils.isNotBlank(apiModelProperty.dictName())){//如果非必填,但是填了
                    field.setAccessible(true);
                    try {
                        Object value = field.get(t);
                        if(value!=null){
                            sb = this.searchDictList(sb,apiModelProperty.dictName(),value,apiModelProperty.value(),field,t);
                            info = sb.toString();
                        }
                    }catch (Exception e){
                        e.printStackTrace(); // 处理异常
                    }

                }
            }
        }
        return info;
    }


    //检查某字典类型的名称是否符合预期要求
    private StringBuilder searchDictList(StringBuilder sb, String dictType, Object value, String apiValue, Field field, Object targetObject) {

//        List<SysDictDTO> sysDictDTOS = AeyeCacheManager.getList(CachePrexConstants.common_dict_list,
//                AeyeReflectionUtil.getFieldName(SysDictDTO::getDicType) + AbstractCacheBOImpl.MERGE_KEY_SPLIT_MARK + dictType,
//                SysDictDTO.class);
//        // 判断 value 是否包含在范围内
//        boolean isValid = false;
//        String dicValue = null; // 用于存储字典值
//        for (SysDictDTO sysDictDTO : sysDictDTOS) {
//            if (sysDictDTO.getDicName().equals(value)) {
//                // 字典名称如果符合预期,将该字段 field 值设置为 sysDictDTO.getDicValue;
//                isValid = true;
//                dicValue = sysDictDTO.getDicValue(); // 获取对应的字典值
//                break;
//            }
//        }
//        if (isValid) {
//            // 设置字段的值为字典值
//            try {
//                field.setAccessible(true); // 允许访问私有属性
//                field.set(targetObject, dicValue); // 设置字段值为字典值
//            } catch (IllegalAccessException e) {
//                e.printStackTrace(); // 处理异常
//            }
//        } else {
//            // 添加错误信息到 importResDTO
//            sb.append(apiValue);
//            sb.append("如");
//            // 列举符合要求的值
//            Set<String> collect = sysDictDTOS.stream().map(SysDictDTO::getDicName).collect(Collectors.toSet());
//            sb.append(collect);
//        }
        return sb;
    }

    public boolean checkLastLine(T t){
        for (Field field : t.getClass().getDeclaredFields()) {
            // 检查是否有 @ApiModelProperty 注解
            if (field.isAnnotationPresent(CheckData.class)) {
                CheckData apiModelProperty = field.getAnnotation(CheckData.class);
                if (apiModelProperty.required()) { // 判断是否必填
                    field.setAccessible(true); // 允许访问私有属性
                    try {
                        Object value = field.get(t);
                        if (value != null) {
                            // 使用 value 属性描述字段名称,添加错误信息到 importResDTO
                            return false;
                        }
                    } catch (IllegalAccessException e) {
                        e.printStackTrace(); // 处理异常
                    }
                }
            }
        }
        return true;
    }
}

实体对象


package com.jiuzhou.desgin.excel.vo;

import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import com.jiuzhou.desgin.excel.annotation.CheckData;
import lombok.Data;

@Data
@ContentRowHeight(15)
@HeadRowHeight(20)
@ColumnWidth(20)
public class StudentScore {

    @CheckData(value = "学生姓名",dictName = "",required = true)
    @ExcelProperty("学生姓名")
    private String studentName;
    @CheckData(value = "学科",dictName = "",required = true)
    @ExcelProperty("学科")
    private String subject;
    @CheckData(value = "成绩",dictName = "",required = true)
    @ExcelProperty("成绩")
    private Integer score;

    // 用于存放批注信息
    @ExcelProperty("批注")
    private String comment;
}

导入结果信息对象

package com.jiuzhou.desgin.excel.dto;

import lombok.Data;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
 */
@Data
public class ImportResDTO implements Serializable {

    private static final long serialVersionUID = -4577699748290750945L;

    private int total = 0;

    private int fail = 0;

    private int succ= 0;

    private String serviceId;

    private Map<Object,Object> errorMap = new HashMap<>();

}

通过构造方法传入对应的bean,然后利用bean调用对应的函数

package com.jiuzhou.desgin.excel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.jiuzhou.desgin.excel.dto.ImportResDTO;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

/**
 */
@Slf4j
public class EasyExcelListener<T> extends AnalysisEventListener<T> {

    /**
     * 读取的头行
     */
    private int sheetNo;

    /**
     * 保存的数据
     */
    private List<T> saveData;

    /**
     * 更新的数据
     */
    private List<T> updateData;

    /**
     * 记录导入的异常信息
     */
    private ImportResDTO importResDTO;
    /**
     * 导入的接口实例
     */
    private ImportService importService;

    /**
     * 元数据信息
     */
    private List<T> sList;
    /**
     * 插入一次量
     */
    private int  BATCH_COUNT = 1000;


    public EasyExcelListener(Integer sheetNo, ImportService importService,List<T> sList) {
        this.sheetNo = sheetNo;
        this.saveData = new ArrayList<>();
        this.updateData = new ArrayList<>();
        this.importResDTO = new ImportResDTO();
        this.importService = importService;
        this.sList = sList;

    }

    @Override
    public void invoke(T t, AnalysisContext context) {
        if(context.readSheetHolder().getSheetNo()!=sheetNo)return;
        if(null==this.sList){
            importService.importCheck(context,this.importResDTO,t,this.saveData,updateData);
        }else{
            importService.importCheck(context,t,importResDTO,this.saveData,this.updateData,this.sList);
        }

        if (saveData.size() >= BATCH_COUNT) {
            importService.saveData(saveData);
            saveData.clear();
        }
        if (updateData.size() >= BATCH_COUNT) {
            importService.updateData(updateData);
            updateData.clear();
        }
    }

    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        if(context.readSheetHolder().getSheetNo()!=sheetNo)return;
        //数据入库逻辑
        if(saveData.size()>0){
            importService.saveData(saveData);
        }
        if(updateData.size()>0){
            importService.updateData(updateData);
        }
        //处理数据解析完成时
        if(importResDTO.getFail()>0){
            String s = importService.uploadMeta(sList);
            this.getRes().setServiceId(s);
        }
    }
    public ImportResDTO getRes() {
        return importResDTO;
    }

    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        super.onException(exception, context);
        log.error(exception.getMessage());
    }
}

二、单元测试

1、正常案例

在这里插入图片描述
导入的demo数据信息,通过postman测试导入结果

{
    "total": 1,
    "fail": 0,
    "succ": 1,
    "serviceId": null,
    "errorMap": {}
}

2、异常案例

在这里插入图片描述

postman测试结果

{
    "total": 1,
    "fail": 1,
    "succ": 0,
    "serviceId": "b96612e0cbec437fabbe895884adeac9",
    "errorMap": {
        "第2行": "成绩不能为空,"
    }
}

三、总结

通常我们在导入的过程中需要对导入的数据进行数据检查并判断对应的明文值和数据库存的值是否一致,这个案例通过查询对应的数据库字典,获取该字段的字典值来判断用户的导入数据是否符合预期,如果我们在不同的案例中根据业务不同,进行导入,我们只需要实现importService接口,并通过spring的指定注入策略来进行导入导出操作,效率会大大提高

四、分享一下

最后源码已上传至源码获取吧!
如果你是搞自媒体的给你推荐一好用网址代码江湖

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值