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

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

项目简述

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

快速开始

引入no-excel依赖

<dependency>
  <groupId>io.github.amerainc</groupId>
  <artifactId>no-excel</artifactId>
  <version>2021.12.24</version>
</dependency>

首先创建一个测试用的实体类(测试样例在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<TestEntity> 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<TestEntity> builder = ExcelWriterBuilder.builder(TestEntity.class);
        try (ExcelWriter<TestEntity> excelWriter = builder.build()) {
            excelWriter.writeDataAndClose(testEntityList, new FileOutputStream("createFile.xls"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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


image-20211222215550582

导出模板

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

    @Test
    public void writeTemplate() {
        //构造模板
        try (ExcelWriter<TestEntity> 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<TestEntity> 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<TestEntity> builder = ExcelReaderBuilder.builder(TestEntity.class);
        try (ExcelReader<TestEntity> excelReader = builder.build(new File(path + "readFile.xls"))) {
            List<TestEntity> testEntities = excelReader.readData();
            System.out.println(testEntities);
        }
    }

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

    @Test
    public void readFile2() {
        String path = getClass().getResource("/").getPath()+"readFile.xls";
        ExcelReaderBuilder<TestEntity> builder = ExcelReaderBuilder.builder(TestEntity.class);
        try (ExcelReader<TestEntity> 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<TestEntity> builder = ExcelReaderBuilder.builder(TestEntity.class);
        try (ExcelReader<TestEntity> 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<? extends StyleProvider> DefaultTitleStyleProvider.class 标题使用的excel样式
headRequireStyle Class<? extends StyleProvider> DefaultHeadRequireStyleProviderProvider.class 当字段为必填时,标题显示的样式
headStyle Class<? extends StyleProvider> DefaultHeadStyleProvider.class 普通表头样式
dataStyle Class<? extends StyleProvider> DefaultDataStyleProvider.class 数据样式

@ExcelField

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

属性 类型 必填 默认值 说明
name String 字段名称,与excel表头对应
sort int 0 字段的输出顺序
require boolean false 字段是否必填,必填时输出表头会带有*号,读取时无必填字段则会抛出异常
converter Class<? extends FieldConverter<?>> 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<Date> {
    /**
     * 输出时的日期格式
     */
    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<Enum<?>> {

    @Override
    @SneakyThrows
    public Map<Enum<?>, String> fieldToExcelDataMap(ExcelFieldMeta excelFieldMeta) {
        Class<Enum<?>> fieldClz = (Class<Enum<?>>) 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<String> implements CascadeProvider {
    @Override
    public Map<String, Map<String, String>> cascadeMap(ExcelFieldMeta excelFieldMeta) {
        Map<String, Map<String, String>> map = new LinkedHashMap<>();
        Map<String, String> red = new LinkedHashMap<>();
        red.put("bred","大红色");
        red.put("sred","小红色");
        Map<String, String> yellow = new LinkedHashMap<>();
        yellow.put("byellow","大黄色");
        yellow.put("syellow","小黄色");
        Map<String, String> 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<TestEntity> builder = ExcelReaderBuilder.builder(TestEntity.class);
        List<TestEntity> testEntities;
        try (ExcelReader<TestEntity> excelReader = builder.build(new File(path))) {
            testEntities = excelReader.readData();
        }
        //过滤成功的数据
        List<TestEntity> success = testEntities.stream().filter(BaseErrMsg::hasNotErrMsg).collect(Collectors.toList());
        System.out.println(success);
        //过滤成功的数据
        List<TestEntity> error = testEntities.stream().filter(BaseErrMsg::hasErrMsg).collect(Collectors.toList());
        //将失败的数据重新导成excel
        try (ExcelWriter<TestEntity> 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<TestEntity> 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);
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341