no-excel 简单实用的excel导入导出工具类(提供了下拉框和级联下拉框,以及常用类型的自动转换)

项目的github地址是https://github.com/amerainc/no-excel,欢迎各位的Star和Issues

项目简述

no-excel是一个excel导入导出工具类,提供基于注解方式使用,并提供了excel下拉框和级联下拉框的生成,同时对基本类型包括时间枚举提供了自动转换的功能,旨在提供简单便捷通用的excel导入导出功能。

快速开始

引入no-excel依赖


  io.github.amerainc
  no-excel
  2021.12.24

首先创建一个测试用的实体类(测试样例在test文件夹下)

@ExcelEntity(title = "测试")
@Data
public class TestEntity{
    //require选项必填时输出会带有*好,读取时无必填字段则会抛出异常
    @ExcelField(name = "必填选项", require = true)
    private String str;
    //在枚举类的情况下,param参数可以指定excel写入读取时使用的枚举类属性
    @ExcelField(name = "枚举类转换", param = "i18n")
    private ColorEnum colorEnum;
    @ExcelField(name = "长整型")
    private Long number;
    @ExcelField(name = "时间")
    private Date date;
}
public enum ColorEnum {
    RED("红色"),
    GOLD("金黄色"),
    YELLOW("黄色");
    String i18n;
    ColorEnum(String i18n){
        this.i18n = i18n;
    }
    public String getI18n() {
        return i18n;
    }
}

excel导出

为了能够重复使用,工具类在导出后并不会主动将workbook关闭,工具类也同时实现了closeable接口,因此建议使用try-resource的方式来使用。

普通导出

   @Test
    public void writeFile() {
        //初始化要写入的数据
        List testEntityList = new ArrayList<>();
        TestEntity testEntity = new TestEntity();
        testEntity.setDate(new Date());
        testEntity.setStr("测试文字");
        testEntity.setNumber(123L);
        testEntity.setColorEnum(ColorEnum.GOLD);
        for (int i = 0; i < 100; i++) {
            testEntityList.add(testEntity);
        }

        //写入
        ExcelWriterBuilder builder = ExcelWriterBuilder.builder(TestEntity.class);
        try (ExcelWriter excelWriter = builder.build()) {
            excelWriter.writeDataAndClose(testEntityList, new FileOutputStream("createFile.xls"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

导出的文件如下图所示,同时枚举类则会生成下拉菜单


image-20211222215550582

导出模板

如果不是为了导出数据,只是为了提供一份供用户填写的模板,也可以直接调用写入模板的方法。

    @Test
    public void writeTemplate() {
        //构造模板
        try (ExcelWriter excelWriter = ExcelWriterBuilder.builder(TestEntity.class).build();
             FileOutputStream fileOutputStream = new FileOutputStream("createTemplate.xls")) {
            excelWriter.writeTemplate(fileOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

导出的文件如下图所示


image-20211222215903643

web导出

同时也提供了web方式的导出

    @GetMapping("/test")
    public void test(HttpServletRequest request, HttpServletResponse response) {
        try (ExcelWriter excelWriter = ExcelWriterBuilder.builder(TestEntity.class).build()) {
            excelWriter.writeTemplateToResponse("测试.xls",request,response);
        }
    }

导出效果如下所示


image-20211224145118494.png

excel导入

直接读取将数据转换成实体类列表返回

    @Test
    public void readFile() {
        String path = getClass().getResource("/").getPath();
        ExcelReaderBuilder builder = ExcelReaderBuilder.builder(TestEntity.class);
        try (ExcelReader excelReader = builder.build(new File(path + "readFile.xls"))) {
            List testEntities = excelReader.readData();
            System.out.println(testEntities);
        }
    }

同时也提供了消费者模式进行消费,消费者模式下将会对数据逐行进行转换,并直接通过消费者进行消费

    @Test
    public void readFile2() {
        String path = getClass().getResource("/").getPath()+"readFile.xls";
        ExcelReaderBuilder builder = ExcelReaderBuilder.builder(TestEntity.class);
        try (ExcelReader excelReader = builder.build(new File(path))) {
           excelReader.readData(testEntity -> {
               System.out.println(testEntity);
           });
        }
    }

如果处理比较耗时,也可以采用多线程的方式分片进行处理

    @Test
    public void readFile3() {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        String path = getClass().getResource("/").getPath()+"readFile.xls";
        ExcelReaderBuilder builder = ExcelReaderBuilder.builder(TestEntity.class);
        try (ExcelReader excelReader = builder.build(new File(path))) {
            //这里将数据分为五片,并交由五个线程处理
            excelReader.readDataConcurrent(testEntity -> {
                System.out.println(testEntity);
            },executorService,5);
        }
    }

注解

@ExcelEntity

在用来承载excel数据的实体类上使用,用于指定一些全局性的参数

@ExcelEntity(title = "测试")
@Data
public class TestEntity{
    //require选项必填时输出会带有*好,读取时无必填字段则会抛出异常
    @ExcelField(name = "必填选项", require = true)
    private String str;
    //在枚举类的情况下,param参数可以指定excel写入读取时使用的枚举类属性
    @ExcelField(name = "枚举类转换", param = "i18n")
    private ColorEnum colorEnum;
    @ExcelField(name = "长整型")
    private Long number;
    @ExcelField(name = "时间")
    private Date date;
}
属性 类型 必填 默认值 说明
title String excel导出时显示的标题名
showTitle boolean true excel导出时是否有标题
showHead boolean true excel导出时是否有表头
maxSize int 500 excel导入时读取的最大数据量限制,超出限制会抛出异常
titleStyle Class DefaultTitleStyleProvider.class 标题使用的excel样式
headRequireStyle Class DefaultHeadRequireStyleProviderProvider.class 当字段为必填时,标题显示的样式
headStyle Class DefaultHeadStyleProvider.class 普通表头样式
dataStyle Class DefaultDataStyleProvider.class 数据样式

@ExcelField

在需要导入导出的字段上进行使用,用于定义字段相关的信息

属性 类型 必填 默认值 说明
name String 字段名称,与excel表头对应
sort int 0 字段的输出顺序
require boolean false 字段是否必填,必填时输出表头会带有*号,读取时无必填字段则会抛出异常
converter Class> DefaultFieldConverter.class 字段转换器,用于提供excel字段和实体类字段之间的转换
param String "" 用于提供给字段转换器额外的处理参数
cascadeDepend String "" 级联依赖字段的字段名(不填默认依赖前一个字段)

功能

字段转换器

FieldConverter

FieldConverter接口用于提供excel字段和实体类字段之间的转换,实现这个接口可以自定义字段转换的规则

parseToField(必须实现)

实现parseToField方法,用于提供excel数据转换成对应字段值的功能,下面以默认的Integer转换器为例

    @Override
    public Integer parseToField(String excelData) {
        try {
            return Integer.parseInt(excelData);
        } catch (NumberFormatException numberFormatException) {
            throw new NoExcelException("必须为整型");
        }
    }

parseToExcelData(必须实现)

实现parseToExcelData方法用于提供将字段值转换为excel数据的功能

    @Override
    public String parseToExcelData(Integer fieldData) {
        return fieldData.toString();
    }

initData

initData方法会在转换器初始化调用,以默认的日期转换器为例,在初始化时可以通过填写注解上param来指定输出到excel的日期格式

public class DefaultDateFieldConverter implements FieldConverter {
    /**
     * 输出时的日期格式
     */
    private String printDateFormat = "yyyy/MM/dd HH:mm:ss";
                                            ......
    @Override
    public String parseToExcelData(Date fieldData) {
        return DateFormatUtil.formatDate(fieldData, this.printDateFormat);
    }

    @Override
    public void initData(ExcelFieldMeta excelFieldMeta) {
        //如果有参数则使用参数作为日期格式
        if (StrUtil.isNotBlank(excelFieldMeta.getParam())) {
            this.printDateFormat = excelFieldMeta.getParam();
        }
    }

match(用于默认字段转换器)

match方法用于默认字段转换器进行匹配使用,如果是在注解上直接指定的转换器则无需实现

    default boolean match(ExcelFieldMeta excelFieldMeta) {
        Class firstGenericType = GenericUtil.getFirstGenericType(this.getClass());
        return firstGenericType != null && firstGenericType.isAssignableFrom(excelFieldMeta.getFieldClz());
    }

order(用于默认字段转换器)

order方法用于决定默认字段转换器进行匹配时优先级越小优先级越高

    default int order() {
        return 0;
    }

isSingleton

项目对字段转换器做了缓存,通过isSingleton觉得当前字段转换器在使用时是否为单例,默认为非单例。如整型等不需要配置的转换器使用了单例,而日期转换器可以自定义输出格式,使用了非单例的形式。

    @Override
    default boolean isSingleton(){
        return false;
    }

DefaultFieldConverter(默认字段转换器)

DefaultFieldConverter是注解中默认使用的字段转换器,DefaultFieldConverter是一个代理类,通过match方法对所有的默认字段转换器进行匹配并实现代理。

DefaultFieldConverter通过spi的方式将所有的默认字段转换器放入匹配列表,并在使用时通过init方法进行初始化匹配到对应的默认字段转换器进行代理,匹配逻辑为由order决定转换器的匹配顺序,并通过match方法匹配使用首个匹配成功的字段转换器

spi如下,目前总共实现了8个默认字段转换器

image-20211223155933042
com.rainc.noexcel.convert.impl.DefaultDateFieldConverter
com.rainc.noexcel.convert.impl.DefaultDoubleFieldConverter
com.rainc.noexcel.convert.impl.DefaultEnumFieldConverter
com.rainc.noexcel.convert.impl.DefaultIntegerFieldConverter
com.rainc.noexcel.convert.impl.DefaultLongFieldConverter
com.rainc.noexcel.convert.impl.DefaultShortFieldConverter
com.rainc.noexcel.convert.impl.DefaultStringFieldConverter
com.rainc.noexcel.convert.impl.DefaultObjectFieldConvert

如果想要自己的字段转换器也能通过DefaultFieldConverter进行代理,则在项目的resoures/META-INF/services下创建文件,文件名为com.rainc.noexcel.convert.FieldConverter,并在文件中写上自定义的字段转换器的全类名

BaseMapFieldConverter(基础字段映射转换器)

BaseMapFieldConverter是字段映射转换器的抽象类,实现BaseMapFieldConverter可以轻松实现枚举字典等具有映射关系的数据转换,并且在导出时可以通过映射值生成excel的下拉框选项

继承BaseMapFieldConverter并实现方法fieldToExcelDataMap,通过实现这个方法并返回一个实体类字段到excel字段的数据映射表

下面用默认的枚举字段转换器为例,返回的键是枚举值,值在没有定义param的情况下默认用name,同时也可以通过param来指定excel的字段。同时使用LinkedHashMap是因为生成下拉框数据时会使用到map的values,用LinkedHashMap可以保证下拉框列表的顺序。

public class DefaultEnumFieldConverter extends BaseMapFieldConverter> {

    @Override
    @SneakyThrows
    public Map, String> fieldToExcelDataMap(ExcelFieldMeta excelFieldMeta) {
        Class> fieldClz = (Class>) excelFieldMeta.getFieldClz();
        String param = excelFieldMeta.getParam();
        Enum[] enums = fieldClz.getEnumConstants();
        return Arrays.stream(enums).collect(Collectors.toMap(anEnum -> anEnum, anEnum -> {
            if (StrUtil.isEmpty(param)) {
                return anEnum.name();
            } else {
                return ReflectUtil.getFieldValue(anEnum, param).toString();
            }
        },(a,b)->a, LinkedHashMap::new));
    }
}

BaseCascadeConverter(基础级联字段转换器)

BaseCascadeConverter是级联字段转换器的抽象类,是在字段映射转换器的基础上实现的,可以用来生成带有级联下拉框的excel字段。

继承BaseCascadeConverter并实现cascadeMap,返回一个格式为Map<级联的excel值,Map<属性值,excel值>>的映射表即可实现级联的下拉框选项

下面实例实现了BaseCascadeConverter,手动创建了一个和之前的ColorEnum进行级联的map数据

public class CascadeConverter extends BaseCascadeConverter implements CascadeProvider {
    @Override
    public Map> cascadeMap(ExcelFieldMeta excelFieldMeta) {
        Map> map = new LinkedHashMap<>();
        Map red = new LinkedHashMap<>();
        red.put("bred","大红色");
        red.put("sred","小红色");
        Map yellow = new LinkedHashMap<>();
        yellow.put("byellow","大黄色");
        yellow.put("syellow","小黄色");
        Map gold = new LinkedHashMap<>();
        gold.put("bgold","大金黄");
        gold.put("sgold","小金黄");
        map.put("红色",red);
        map.put("黄色", yellow);
        map.put("金黄色",gold);
        return map;
    }
}

在测试用的实体类中增加级联字段用来测试,将converter设置为自定义的CascadeConverter同时将级联的依赖字段选为枚举类转换字段

@ExcelEntity(title = "测试")
@Data
public class TestEntity{
    //require选项必填时输出会带有*号,读取时无必填字段则会抛出异常
    @ExcelField(name = "必填选项", require = true)
    private String str;
    //在枚举类的情况下,param参数可以指定excel写入读取时使用的枚举类属性
    @ExcelField(name = "枚举类转换", param = "i18n")
    private ColorEnum colorEnum;
    @ExcelField(name = "长整型")
    private Long number;
    @ExcelField(name = "时间")
    private Date date;
    @ExcelField(name = "级联",converter = CascadeConverter.class,cascadeDepend = "枚举类转换")
    private String cascade;
}

使用ExcelWriter的writeTemplate方法导出excel模板并查看,级联效果如下

image-20211223200459788
image-20211223200536248

错误行校验

工作中经常有业务是用户直接通过excel的形式进行批量的导入操作,由于excel不可控,所以总是会产生一些错误的数据,错误行校验功能与字段转换功能紧密贴合,在字段转换时达到字段的校验功能

需要使用错误行校验首先实体类继承BaseErrMsg,BaseErrMsg用来存储校验的错误信息

@ExcelEntity(title = "测试")
@Getter
@Setter
@ToString
public class TestEntity extends BaseErrMsg {
    //require选项必填时输出会带有*号,读取时无必填字段则会抛出异常
    @ExcelField(name = "必填选项", require = true)
    private String str;
    //在枚举类的情况下,param参数可以指定excel写入读取时使用的枚举类属性
    @ExcelField(name = "枚举类转换", param = "i18n")
    private ColorEnum colorEnum;
    @ExcelField(name = "长整型")
    private Long number;
    @ExcelField(name = "时间")
    private Date date;
    @ExcelField(name = "级联",converter = CascadeConverter.class,cascadeDepend = "枚举类转换")
    private String cascade;
}

然后再字段转换器中校验不符合的地方抛出NoExcelException异常,工具类就会捕获这个异常并写入到BaseErrMsg的属性中,以默认长整型转换器为例,在解析Integer失败时抛出了必须为长整型的异常

    @Override
    public Long parseToField(String excelData) {
        try {
            return Long.parseLong(excelData);
        } catch (NumberFormatException numberFormatException) {
          throw new NoExcelException("必须为长整型");
        }
    }

这里尝试读取下面的数据,下面第一行数据有两处错误,必填选项未填,长整型也不对

image-20211224102848491

下面的代码读取了这个失败文件并将成功的数据和失败的数据分离,在控制台输出成功的数据,并将失败数据导出到errFile.xls

 @Test
    public void readerrorFile() {
        //读取excel
        String path = getClass().getResource("/").getPath()+"readerrFile.xls";
        ExcelReaderBuilder builder = ExcelReaderBuilder.builder(TestEntity.class);
        List testEntities;
        try (ExcelReader excelReader = builder.build(new File(path))) {
            testEntities = excelReader.readData();
        }
        //过滤成功的数据
        List success = testEntities.stream().filter(BaseErrMsg::hasNotErrMsg).collect(Collectors.toList());
        System.out.println(success);
        //过滤成功的数据
        List error = testEntities.stream().filter(BaseErrMsg::hasErrMsg).collect(Collectors.toList());
        //将失败的数据重新导成excel
        try (ExcelWriter excelWriter = ExcelWriterBuilder.builder(TestEntity.class).build()) {
            excelWriter.writeDataAndClose(error,new FileOutputStream("errFile.xls"));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

可以看到结果如下图,控制台输出了成功的数据行,而excel中的导出了错误行,并且含有错误行的错误信息

image-20211224103515446
image-20211224103621184

忽略字段

有时候导入导出希望动态的选择某些字段,而不是对标有注解的字段进行全量的导入导出,比如继承了BaseErrMsg后,在生成模板或导入数据时并不希望有错误信息行的出现,导入数据时也不希望读到excel中的错误信息

ignoreWithFieldName

通过Builder上提供的ignoreWithFieldName方法可以在导入导出时忽略这些字段,列如忽略错误信息经常用到,因此也提供了一个快捷方法

    public Builder ignoreErrMsg() {
        return this.ignoreWithFieldName(BaseErrMsg::getErrMsg);
    }

使用如下,忽略指定行后构建实例,即可

    @Test
    public void writeTemplate() {
        //构造模板
        try (ExcelWriter excelWriter = ExcelWriterBuilder.builder(TestEntity.class)
                //忽略errMsg行
                .ignoreErrMsg()
                //构建
                .build();
             FileOutputStream fileOutputStream = new FileOutputStream("createTemplate.xls")) {
            excelWriter.writeTemplate(fileOutputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

修改样式

StyleProvider

通过实现StyleProvider可以自定义标题,表头,数据的样式

editStyle

实现editStyle方法能够修改单元格样式

    @Override
    public void editStyle(CellStyle style, Workbook workbook) {
        style.setFillForegroundColor(HSSFColor.HSSFColorPredefined.GREY_25_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        style.setAlignment(HorizontalAlignment.CENTER);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
    }

editFont

实现editFont方法能够修改字体样式

   @Override
    public void editFont(Font font) {
        font.setBold(Boolean.FALSE);
        font.setFontHeightInPoints((short)10);
    }

你可能感兴趣的:(no-excel 简单实用的excel导入导出工具类(提供了下拉框和级联下拉框,以及常用类型的自动转换))