EasyExcel基本使用

用来实现系统的导出和导入

一、准备工作

<!--easyexcel依赖-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>3.0.0-beta1</version>
</dependency>

二、注解意思

//用于匹配excel和实体类的匹配
@ExcelProperty
名称 描述
value 用于匹配excel中的头,必须全匹配,如果有多行头,会匹配最后一行头
order 优先级高于value,会根据order的顺序来匹配实体和excel中数据的顺序(很少用)
index 优先级高于valueorder,会根据index直接指定到excel中具体的哪一列
converter 指定当前字段用什么转换器
//默认所有字段都会和excel去匹配,加了这个注解会忽略该字段;不加则用字段名去当excel的列名;加在字段上面
@ExcelIgnore
//默认不加@ExcelProperty注解的都会参与读写,加了@ExcelIgnoreUnannotated不会参与;加在类名上面
@ExcelIgnoreUnannotated
//自带日期格式转换(将String类型的日期转换成自定义模板的Date类型日期)
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date date;

//自带数字转换(将String类型的数字转换为自定义模板的数字类型数字)
@NumberFormat(pattern="#,###,###.#")
private Float number;

三、导出

1、为导出对象添加注解

@Data
@TableName("t_user")
public class User {

    @TableId
    private Integer id;

    @TableField("name")
    @ExcelProperty(index = 0 , value = "姓名")
    private String name;

    @TableField("sex")
    @ExcelProperty(index = 1 , value = "性别")
    private Short sex;

    @TableField("age")
    @ExcelProperty(index = 2 , value = "年龄")
    private Integer age;

    @TableField("birthday")
    @ExcelProperty(index = 3 , value = "出生日期")
    private Date birthday;

    @TableField("money")
    @ExcelProperty(index = 4 , value = "薪资")
    private double money;
}

2、编写工具类

//导出
public static <T> void easyExportWithModel(HttpServletResponse response, List<T> records, Class<T> classname, String fileName) throws IOException {
    String ext = ExcelTypeEnum.XLSX.getValue();
    response.setContentType("application/ms-excel;charset=UTF-8");
    response.setHeader("Content-Disposition", "attachment;filename="
                       .concat(String.valueOf(URLEncoder.encode(fileName + ext, "UTF-8"))));

    try {
        EasyExcel.write(response.getOutputStream(), classname).sheet("sheet1").doWrite(records);
    } catch (IOException e) {
        // 重置response
        response.reset();
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        Map<String, String> map = new HashMap<String, String>(16);
        map.put("status", "failure");
        map.put("message", "下载文件失败" + e.getMessage());
        response.getWriter().println(JSON.toJSONString(map));
    }
}

3、编写controller

//导出
@PostMapping("export")
public Result export(@RequestBody PageDTO dto, HttpServletResponse response) throws IOException {
    String fileName = "用户名单";
    dto.setSize(3000);
    List<User> userList = userService.list(dto).getRecords();
    if(CollectionUtils.isEmpty(userList)){
        return new Result("400",Message.EXPORT_FAIL);
    }
    EasyExcelUtil.easyExportWithModel(response, userList, User.class, fileName);
    return new Result("200",Message.EXPORT_SUCCESS);
}

4、结果

存在问题:

  1. 出生日期列没有展示数据
  2. 性别没有从枚举类型转成男女
  3. id列不想展示出来却出现了

5、问题优化

1、出生日期列没有展示数据

原因:是因为列的宽度没处理好才导致数据没展示出来

//@ContentRowHeight(30)  表体行高
//@HeadRowHeight(20)  	 表头行高
@Data
@ColumnWidth(20)		//将所有列的宽都定义为20,也可以加在某个字段上(将对应列的宽定义为20)
@TableName("t_user")
public class User {

    @TableId
    private Integer id;

    @TableField("name")
    @ExcelProperty(index = 0 , value = "姓名")
    private String name;

    @TableField("sex")
    @ExcelProperty(index = 1 , value = "性别")
    private Short sex;

    @TableField("age")
    @ExcelProperty(index = 2 , value = "年龄")
    private Integer age;

    @TableField("birthday")
    @ExcelProperty(index = 3 , value = "出生日期")
    private Date birthday;

    @TableField("money")
    @ExcelProperty(index = 4 , value = "薪资")
    private double money;
}
2、性别没有从枚举类型转成男女

原因:需要自己去自定义一个转换器来将数字类型转为字符串类型,EasyExcel提供了两个注解@DateTimeFormat和@NumberFormat,自定义日期以及数字的格式化转化。

@TableField("birthday")
@DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
@ExcelProperty(index = 3 , value = "出生日期")
private Date birthday;

@TableField("money")
@NumberFormat("#.##¥")
@ExcelProperty(index = 4 , value = "薪资")
private double money;

结果:

也可以自定义转换器

1、新建枚举类
@Getter
@AllArgsConstructor
public enum SexEnum {
    MAN(1,"男生"),
    WOMAN(2,"女生");

    private final Integer number;
    private final String description;
}
2、自定义转换器
//Converter<Short> 这里的Short是指,数据库存的字段是Short类型
public class SexEnumConvert implements Converter<Short> {
    @Override
    public Class<?> supportJavaTypeKey() {
        return null;
    }

    @Override
    public CellDataTypeEnum supportExcelTypeKey() {
        return null;
    }

    //excel转java
    @Override
    public Short convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return SexEnum.MAN.getDescription().equals(cellData.getStringValue()) ? SexEnum.MAN.getNumber().shortValue() : SexEnum.WOMAN.getNumber().shortValue();
    }

    //java转excel
    @Override
    public WriteCellData<?> convertToExcelData(Short value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception {
        return new WriteCellData((value == SexEnum.MAN.getNumber().shortValue())? SexEnum.MAN.getDescription() : SexEnum.WOMAN.getDescription());
    }
}
3、在对象的注解上添加转换
@TableField("sex")
@ExcelProperty(index = 1 , value = "性别" , converter = SexEnumConvert.class)
private Short sex;
4、结果

3、id列不想展示出来却出现了

原因:默认不加@ExcelProperty注解的都会参与读写,假如不想展示,则在对应的字段加上注解

@TableId
@ExcelIgnore
private Integer id;

或者是在对象上加个全局的注解

@Data
@ColumnWidth(30)
@ExcelIgnoreUnannotated
@TableName("t_user")
public class User {

    @TableId
    private Integer id;

    @TableField("name")
    @ExcelProperty(index = 0 , value = "姓名")
    private String name;

    @TableField("sex")
    @ExcelProperty(index = 1 , value = "性别" , converter = SexEnumConvert.class)
    private Short sex;

    @TableField("age")
    @ExcelProperty(index = 2 , value = "年龄")
    private Integer age;

    @TableField("birthday")
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(index = 3 , value = "出生日期")
    private Date birthday;

    @TableField("money")
    @NumberFormat("#.##¥")
    @ExcelProperty(index = 4 , value = "薪资")
    private double money;
}

结果:

四、导出的高级用法

1、复杂头导出

//格式为
public class ComplexHeadData {
    @ExcelProperty({"主标题", "字符串标题"})
    private String string;
    @ExcelProperty({"主标题", "日期标题"})
    private Date date;
    @ExcelProperty({"主标题", "数字标题"})
    private Double doubleData;
}
@Data
@ColumnWidth(30)
@ExcelIgnoreUnannotated
@TableName("t_user")
public class User {

    @TableId
    private Integer id;

    @TableField("name")
    @ExcelProperty(index = 0 , value = {"基本信息","姓名"})
    private String name;

    @TableField("sex")
    @ExcelProperty(index = 1 , value = {"基本信息","性别"} , converter = SexEnumConvert.class)
    private Short sex;

    @TableField("age")
    @ExcelProperty(index = 2 , value = {"基本信息","年龄"})
    private Integer age;

    @TableField("birthday")
    @DateTimeFormat("yyyy年MM月dd日HH时mm分ss秒")
    @ExcelProperty(index = 3 , value = "出生日期")
    private Date birthday;

    @TableField("money")
    @NumberFormat("#.##¥")
    @ExcelProperty(index = 4 , value = "薪资")
    private double money;
}

结果:

2、合并单元格导出

// 将第6-7行的2-3列合并成一个单元格
// @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2)
public class DemoMergeData {
    // 这一列 每隔2行 合并单元格
    @ContentLoopMerge(eachRow = 2)
    @ExcelProperty("字符串标题")
    private String string;
    @ExcelProperty("日期标题")
    private Date date;
    @ExcelProperty("数字标题")
    private Double doubleData;
}

五、导入

1、为导入对象添加注解

@Data
@TableName("t_user")
public class UserImport {

    @TableId(type = IdType.AUTO)
    private Integer id;
    // 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
    // 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
    @TableField("name")
    @ExcelProperty(index = 0)
    private String name;

    @TableField("sex")
    @ExcelProperty(index = 1 , converter = SexEnumConvert.class)
    private Short sex;

    @TableField("age")
    @ExcelProperty(index = 2)
    private Integer age;

    @TableField("birthday")
    @ExcelProperty(index = 3)
    @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
    private Date birthday;

    @TableField("money")
    @NumberFormat("#.##")
    @ExcelProperty(index = 4)
    private double money;
}

2、编写工具类

public static <T> void importData(MultipartFile file, Class<T> cls, ReadListener listener, Boolean readAll) throws Exception {
        if (file.isEmpty() && file.getSize() == 0) {
            throw new ServiceException("导入的文件为空!");
        }
        // 文件名合法校验
        FileUtils.assertAllowed(file, MimeTypeUtils.EXCEL_EXTENSION);
        InputStream inputStream = file.getInputStream();
        ExcelReader excelReader = null;
        if (readAll) {
            // 读取excel文件,全部sheet
            // 这里需要注意listener的doAfterAllAnalysed 会在每个sheet读取完毕后调用一次。然后所有sheet都会往同一个listener里面写
            EasyExcel.read(inputStream, cls, listener).doReadAll();
        } else {
            try {
            // 读取部分sheet
            excelReader = EasyExcel.read(inputStream).build();
            // 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener
            ReadSheet readSheet1 =
                    EasyExcel.readSheet(0).head(cls).registerReadListener(listener).build();
            ReadSheet readSheet2 =
                    EasyExcel.readSheet(1).head(cls).registerReadListener(listener).build();
            // 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能
            excelReader.read(readSheet1, readSheet2);
            } finally {
                if (excelReader != null) {
                    // 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的
                    excelReader.finish();
                }
            }
        }
    }
/**
  * 默认大小 50M
  */
public static final long DEFAULT_MAX_SIZE = 30 * 1024 * 1024;

/**
  * 默认的文件名最大长度 100
  */
public static final int FILE_NAME_MAX = 100;

//校验文件名合法性
public static final void assertAllowed(MultipartFile file, String[] allowedExtension)
    throws Exception {

    if (file.getOriginalFilename() != null) {
        int fileNamelength = file.getOriginalFilename().length();

        if (fileNamelength > FILE_NAME_MAX) {
            throw new Exception("文件名过长");
        }
    }

    long size = file.getSize();
    if (size > DEFAULT_MAX_SIZE) {
        throw new Exception("文件过大");
    }

    String extension = getExtension(file);
    if (allowedExtension != null && !isAllowedExtension(extension, allowedExtension)) {
        throw new Exception("请上传指定类型的文件!");
    }

}


	/**
     * 判断MIME类型是否是允许的MIME类型
     *
     * @param extension
     * @param allowedExtension
     * @return
     */
public static final boolean isAllowedExtension(String extension, String[] allowedExtension) {
    for (String str : allowedExtension) {
        if (str.equalsIgnoreCase(extension)) {
            return true;
        }
    }
    return false;
}


	/**
     * 获取文件名的后缀
     *
     * @param file 表单文件
     * @return 后缀名
     */
public static final String getExtension(MultipartFile file) {
    String fileName = file.getOriginalFilename();
    String extension = null;
    if (fileName == null) {
        return null;
    } else {
        int index = indexOfExtension(fileName);
        extension = index == -1 ? "" : fileName.substring(index + 1);
    }

    if (StringUtils.isEmpty(extension)) {
        extension = MimeTypeUtils.getExtension(file.getContentType());
    }
    return extension;
}
/**
 * 媒体类型工具类
 *
 * @author ruoyi
 */
public class MimeTypeUtils
{
    public static final String IMAGE_PNG = "image/png";

    public static final String IMAGE_JPG = "image/jpg";

    public static final String IMAGE_JPEG = "image/jpeg";

    public static final String IMAGE_BMP = "image/bmp";

    public static final String IMAGE_GIF = "image/gif";

    public static final String[] IMAGE_EXTENSION = { "bmp", "gif", "jpg", "jpeg", "png" };

    public static final String[] FLASH_EXTENSION = { "swf", "flv" };

    public static final String[] MEDIA_EXTENSION = { "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
            "asf", "rm", "rmvb" };

    public static final String[] VIDEO_EXTENSION = { "mp4", "avi", "rmvb" };

    public static final String[] DEFAULT_ALLOWED_EXTENSION = {
            // 图片
            "bmp", "gif", "jpg", "jpeg", "png",
            // word excel powerpoint
            "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
            // 压缩文件
            "rar", "zip", "gz", "bz2",
            // 视频格式
            "mp4", "avi", "rmvb",
            // pdf
            "pdf" };

    public static final String[] EXCEL_EXTENSION = { "xls", "xlsx" };


    public static final String[] DEFECT_CENTER_EXTENSION = { "xls", "xlsx", "pdf", "doc", "docx" };


    public static String getExtension(String prefix)
    {
        switch (prefix)
        {
            case IMAGE_PNG:
                return "png";
            case IMAGE_JPG:
                return "jpg";
            case IMAGE_JPEG:
                return "jpeg";
            case IMAGE_BMP:
                return "bmp";
            case IMAGE_GIF:
                return "gif";
            default:
                return "";
        }
    }
}

3、编写监听器

//有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
@Component
public class UserListener extends AnalysisEventListener<UserImport> {

    private static UserImportService userImportService;
    
    @Autowired
    public void setUserImportService(UserImportService userImportService) {
        UserListener.userImportService = userImportService;
    }

    /**
     * 每隔3000条存储数据库,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 3000;
    private List<UserImport> userImportList = new ArrayList<>();

    //这个每一条数据解析都会来调用
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void invoke(UserImport userImport, AnalysisContext analysisContext) {
        //判断一条数据是否为空,为空则停止
        if(isLineNullValue(userImport)){
            return;
        }
        
        //这里就可以进行每一条数据的单元格数据校验
	    //这里可以写一些逻辑复杂的校验
        
        log.info("解析到一条数据:{}", JSON.toJSONString(userImport));
        userImportList.add(userImport);
        if (userImportList.size() >= BATCH_COUNT) {
            saveUser();
            userImportList.clear();
        }
    }

    //所有数据解析完成了,都会来调用
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveUser();
        log.info("所有数据解析完成!");
    }
    
	//数据转换异常时的处理
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        //异常属于excel数据转换的
        if (exception instanceof ExcelDataConvertException) {
            Integer columnIndex = ((ExcelDataConvertException) exception).getColumnIndex() + 1;
            Integer rowIndex = ((ExcelDataConvertException) exception).getRowIndex() + 1;
            String message = String.format("第%s行,第%s列数据格式有误,请核实", rowIndex, columnIndex);
            throw new ServiceException(message, Message.EXPORT_FAIL_CODE);
        } else if (exception instanceof RuntimeException) {
            throw exception;
        } else {
            super.onException(exception, context);
        }
    }

    /**
     * 将数据存入数据库
     */
    public void saveUser() {
        log.info("{}条数据,开始存储数据库!", userImportList.size());
        userImportService.saveBatch(userImportList,BATCH_COUNT);
        log.info("存储数据库成功!");
    }

    /**
     * 判断整行单元格数据是否均为空
     */
    private boolean isLineNullValue(UserImport data) {
        try {
            List<Field> fields = Arrays.stream(data.getClass().getDeclaredFields())
                    .filter(f -> f.isAnnotationPresent(ExcelProperty.class))
                    .collect(Collectors.toList());
            List<Boolean> lineNullList = new ArrayList<>(fields.size());
            for (Field field : fields) {
                field.setAccessible(true);
                Object value = field.get(data);
                if (Objects.isNull(value)) {
                    lineNullList.add(Boolean.TRUE);
                } else {
                    lineNullList.add(Boolean.FALSE);
                }
            }
            return lineNullList.stream().allMatch(Boolean.TRUE::equals);
        } catch (Exception e) {
            log.error("读取数据行[{}]解析失败: {}", data, e.getMessage());
        }
        return true;
    }
}

4、编写controller

//导入
@PostMapping("import")
public Result importExcel(@RequestParam MultipartFile excelFile) throws Exception {
    EasyExcelUtil.importData(excelFile, UserImport.class, new UserListener(), true);
    return new Result("200",Message.IMPORT_SUCCESS);
}

5、结果

存在问题:

Excel中日期列假如为1998.12.1等不是日期类型的,会报错

6、问题优化

1、注解法

@TableField("birthday")
@ExcelProperty(index = 3)
//接收excel对应单元格为yyyy-MM-dd HH:mm:ss的字符串,不是这格式会报错
//若要接收1998.12.1格式,则
//@DateTimeFormat("yyyy.MM.dd")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")							
private Date birthday;			

2、创多一个实体与导出实体区分开

//实体(当数据库的字段定义为datetime类型,入库会自动帮你转),但出库的数据则会定死格式,不会帮你转成你想要的日期格式
@TableField("birthday")
@ExcelProperty(index = 3)
//若要接收1998.12.1格式,则
//@DateTimeFormat("yyyy.MM.dd")
@DateTimeFormat("yyyy-MM-dd HH:mm:ss")		
private String birthday;

六、导入校验问题

//写个业务异常类
public final class ServiceException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    /**
     * 错误码
     */
    private String code;

    /**
     * 错误提示
     */
    private String message;

    /**
     * 错误明细,内部调试错误
     * <p>
     * 和 一致的设计
     */
    private String detailMessage;

    /**
     * 空构造方法,避免反序列化问题
     */
    public ServiceException() {
    }

    public ServiceException(String message) {
        this.message = message;
    }

    public ServiceException(String message, String code) {
        this.message = message;
        this.code = code;
    }

    public ServiceException(String detailMessage,String message, String code) {
        this.detailMessage = detailMessage;
        this.message = message;
        this.code = code;
    }

    public String getDetailMessage() {
        return detailMessage;
    }

    public String getMessage() {
        return message;
    }

    public String getCode() {
        return code;
    }

    public ServiceException setMessage(String message) {
        this.message = message;
        return this;
    }

    public ServiceException setDetailMessage(String detailMessage) {
        this.detailMessage = detailMessage;
        return this;
    }

}

1、日期检验

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelFormatValid {
	//自己定义该注解下所接收的模板
    String format() default "yyyy-MM-dd";
    String message() default "时间填写不符合规范!";
}

2、字符串长度校验

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelStrValid {
    //自己定义长度
    int length() default 0;
    String message() default "文字填写超出长度要求";
}

3、单元格必填校验

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelValid {
    String message() default "导入有未填入的字段";
}

4、Decimal类型校验

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelDecimalValid {
    String min();
    String max();
    String message() default "小数类型数字填写超出范围";
}

5、Int类型校验

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelIntValid {
    int min();
    int max();
    String message() default "整数数字填写超出范围";
}

6、自己写个校验工具类

//校验excel每条数据
@Component
public class ExcelImportValidUtil {
    /**
     * Excel导入字段校验
     * @param object 校验的JavaBean 其属性须有自定义注解
     */
    public void valid(Object object, AnalysisContext analysisContext) {
        Field[] fields = object.getClass().getDeclaredFields();
        for (Field field : fields) {
            //设置可访问
            field.setAccessible(true);
            //属性的值
            Object fieldValue;

            try {
                fieldValue = field.get(object);
            } catch (IllegalAccessException e) {
                throw new ServiceException(String.format("第%s行编号为空,请核实", analysisContext.readRowHolder().getRowIndex() + 1) + " | " + field.getAnnotation(ExcelValid.class).message(), Message.IMPORT_FAIL_CODE);
            }

            //是否包含必填校验注解
            boolean isExcelValid = field.isAnnotationPresent(ExcelValid.class);
            if (isExcelValid && Objects.isNull(fieldValue)) {
                throw new ServiceException(String.format("第%s行字段为空,请核实", analysisContext.readRowHolder().getRowIndex() + 1) + " | " + field.getAnnotation(ExcelValid.class).message(), Message.IMPORT_FAIL_CODE);
            }

            // 是否包含日期格式校验注解
            boolean isExcelDateFormatValid = field.isAnnotationPresent(ExcelFormatValid.class);
            if (isExcelDateFormatValid) { // 若输入的时间字段被excel自动格式化了
                if(!(fieldValue instanceof Date)) {
                    throw new ServiceException(String.format("第%s行字段格式错误,请核实", analysisContext.readRowHolder().getRowIndex() + 1) + " | " + field.getAnnotation(ExcelFormatValid.class).message(), Message.IMPORT_FAIL_CODE);
                } else { // 若没被excel转换,则传入的为String类型
                    String cellStr = fieldValue.toString();
                    String df = field.getAnnotation(ExcelFormatValid.class).format();
                    DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(df);
                    try {
                        dateTimeFormatter.parse(cellStr);  // 如果报错则说明输入的时间格式有问题
                    }catch (Exception e) {
                        throw new ServiceException(String.format("第%s行字段格式错误,请核实", analysisContext.readRowHolder().getRowIndex() + 1) + " | " + field.getAnnotation(ExcelFormatValid.class).message(),Message.IMPORT_FAIL_CODE);
                    }
                }
            }

            //是否包含字符串长度校验注解
            boolean isExcelStrValid = field.isAnnotationPresent(ExcelStrValid.class);
            if (isExcelStrValid) {
                String cellStr = fieldValue.toString();
                int length = field.getAnnotation(ExcelStrValid.class).length();
                if (StringUtils.isNotBlank(cellStr) && cellStr.length() > length) {
                    throw new ServiceException(String.format("第%s行字段过长错误,请核实", analysisContext.readRowHolder().getRowIndex() + 1) + " | " + field.getAnnotation(ExcelStrValid.class).message(),Message.IMPORT_FAIL_CODE);
                }
            }
            //是否包含int类型校验注解
            boolean isExcelIntValid = field.isAnnotationPresent(ExcelIntValid.class);
            if (isExcelIntValid) {
                if (fieldValue instanceof Integer) {
                    int cellInt = Integer.parseInt(fieldValue.toString());
                    int min = field.getAnnotation(ExcelIntValid.class).min();
                    int max = field.getAnnotation(ExcelIntValid.class).max();
                    if (cellInt < min || cellInt > max) {
                        throw new ServiceException(String.format("第%s行字段过大,请核实", analysisContext.readRowHolder().getRowIndex() + 1) + " | " + field.getAnnotation(ExcelIntValid.class).message(),Message.IMPORT_FAIL_CODE);
                    }
                }
            }
            //是否包含decimal类型注解
            boolean isExcelDecimalValid = field.isAnnotationPresent(ExcelDecimalValid.class);
            if (isExcelDecimalValid) {
                if (isBigDecimal(fieldValue.toString())) {
                    BigDecimal cellDecimal = new BigDecimal(fieldValue.toString());
                    BigDecimal min = new BigDecimal(field.getAnnotation(ExcelDecimalValid.class).min());
                    BigDecimal max = new BigDecimal(field.getAnnotation(ExcelDecimalValid.class).max());
                    if (cellDecimal.compareTo(min) < 0 || cellDecimal.compareTo(max) > 0) {
                        throw new ServiceException(String.format("第%s行字段过大,请核实", analysisContext.readRowHolder().getRowIndex() + 1) + " | " + field.getAnnotation(ExcelDecimalValid.class).message(),Message.IMPORT_FAIL_CODE);
                    }
                } else {
                    throw new ServiceException(String.format("第%s行字段类型错误,请核实", analysisContext.readRowHolder().getRowIndex() + 1) + " | " + "不是小数数字类型",Message.IMPORT_FAIL_CODE);
                }
            }
        }
    }

    private static boolean isBigDecimal(String decimal) {
        try {
            BigDecimal bd = new BigDecimal(decimal);
            return true;
        } catch (NumberFormatException e) {
            return false;
        }
    }
}

7、监听器补加校验

//有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
@Slf4j
@Component
public class UserListener extends AnalysisEventListener<UserImport> {

    private static UserImportService userImportService;
    private static ExcelImportValidUtil excelImportValidUtil;

    @Autowired
    public void setUserImportService(UserImportService userImportService) {
        UserListener.userImportService = userImportService;
    }

    @Autowired
    public void setExcelImportValidUtil(ExcelImportValidUtil excelImportValidUtil) {
        UserListener.excelImportValidUtil = excelImportValidUtil;
    }

    /**
     * 每隔3000条存储数据库,然后清理list ,方便内存回收
     */
    private static final int BATCH_COUNT = 3000;
    private List<UserImport> userImportList = new ArrayList<>();

    //这个每一条数据解析都会来调用
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void invoke(UserImport userImport, AnalysisContext analysisContext) {
        //判断一条数据是否为空,为空则停止
        if(isLineNullValue(userImport)){
            return;
        }
        excelImportValidUtil.valid(userImport,analysisContext);
        //通常这里来写校验

        //这里就可以进行每一条数据的单元格数据校验
        log.info("解析到一条数据:{}", JSON.toJSONString(userImport));
        userImportList.add(userImport);
        if (userImportList.size() >= BATCH_COUNT) {
            saveUser();
            userImportList.clear();
        }
    }

    //所有数据解析完成了,都会来调用
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        // 这里也要保存数据,确保最后遗留的数据也存储到数据库
        saveUser();
        log.info("所有数据解析完成!");
    }

    //数据转换异常时的处理
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        //异常属于excel数据转换的
        if (exception instanceof ExcelDataConvertException) {
            Integer columnIndex = ((ExcelDataConvertException) exception).getColumnIndex() + 1;
            Integer rowIndex = ((ExcelDataConvertException) exception).getRowIndex() + 1;
            String message = String.format("第%s行,第%s列数据格式有误,请核实", rowIndex, columnIndex);
            throw new ServiceException(message, Message.EXPORT_FAIL_CODE);
        } else if (exception instanceof RuntimeException) {
            throw exception;
        } else {
            super.onException(exception, context);
        }
    }

    /**
     * 将数据存入数据库
     */
    public void saveUser() {
        log.info("{}条数据,开始存储数据库!", userImportList.size());
        userImportService.saveBatch(userImportList,BATCH_COUNT);
        log.info("存储数据库成功!");
    }

    /**
     * 判断整行单元格数据是否均为空
     */
    private boolean isLineNullValue(UserImport data) {
        try {
            List<Field> fields = Arrays.stream(data.getClass().getDeclaredFields())
                    .filter(f -> f.isAnnotationPresent(ExcelProperty.class))
                    .collect(Collectors.toList());
            List<Boolean> lineNullList = new ArrayList<>(fields.size());
            for (Field field : fields) {
                field.setAccessible(true);
                Object value = field.get(data);
                if (Objects.isNull(value)) {
                    lineNullList.add(Boolean.TRUE);
                } else {
                    lineNullList.add(Boolean.FALSE);
                }
            }
            return lineNullList.stream().allMatch(Boolean.TRUE::equals);
        } catch (Exception e) {
            log.error("读取数据行[{}]解析失败: {}", data, e.getMessage());
        }
        return true;
    }
}

8、导入对象加上校验注解

@Data
@TableName("t_user")
public class UserImport {

    @TableId(type = IdType.AUTO)
    private Integer id;
    // 这里不建议 index 和 name 同时用,要么一个对象只用index,要么一个对象只用name去匹配
    // 用名字去匹配,这里需要注意,如果名字重复,会导致只有一个字段读取到数据
    @TableField("name")
    @ExcelProperty(index = 0)
    private String name;

    @ExcelValid
    @TableField("sex")
    @ExcelProperty(index = 1 , converter = SexEnumConvert.class)
    private Short sex;

    @ExcelValid
    @TableField("age")
    @ExcelProperty(index = 2)
    private Integer age;

    @ExcelValid
    @TableField("birthday")
    @ExcelProperty(index = 3)
    @DateTimeFormat("yyyy-MM-dd")
    private Date birthday;

    @ExcelValid
    @TableField("money")
    @NumberFormat("#.##")
    @ExcelProperty(index = 4)
    private double money;

}

9、结果

异常捕捉成功!

posted @ 2022-07-28 18:24  啊俊同学  阅读(2486)  评论(0)    收藏  举报