基于Adapter的Excel库

前言

       Excel的功能应该说很常见了,也有很多有名的库,比如说阿里的EasyExcel等。
       之前一段时间在项目上也遇到了excel的相关功能,再早些时候其实也接触过poi这块,只不过基本上都是复制粘贴改一改能用就行,那时应该还没有一些比较好的库。然后这次遇上的时候我也上github上找了找有没有什么成熟的库(话说poi难道还不成熟???)主要还是阿里的star比较多。
       之后我就看了看EasyExcel的用法,说实话,我感觉高级用法的文档有点缺,可能是我没找到或者文档还没有完善,至少我当时想的,如果我有一些特殊的数据需要自定义一些单元格的数据该怎么办。毕竟会有这种情况,但是我大致看了也没发现有对应的文档所以就没有用他们的库。
       再加上当时我正好有一个思路,就打算自己封装一个看看,于是就有了jpoi这个库,严格来说还不能说库,只能说一个待完善的工具。所以这篇文章我就主要讲述一下封装的思路。
       大家可以想象一下excel的文件,是不是就是有很多单元格然后每个单元格填充了一些数据。因为我之前是做Android的,所以我突然发现这和Android里面的GridView特别的像,也就是网格布局。其实熟悉Android的人都知道,Android里面类似这种list或grid的数据填充都是使用adapter的模式,而excel其实就相当于给每一个单元格(View)进行数据填充。

思路分析

  • Adapter为核心
public interface WriteAdapter {

    Object getData(int sheet, int row, int cell);//对应单元格的数据

    String getSheetName(int sheet);//对应sheet的名称

    int getSheetCount();//有多少sheet

    int getRowCount(int sheet);//对应的sheet有多少行

    int getCellCount(int sheet, int row);//对应sheet和对应行有多少列
}

public interface ReadAdapter {

    void readCell(Object value, int s, int r, int c, int sCount, int rCount, int cCount);//对应位置的数据

    Object getValue();//读取的数据
}

首先是来说写excel,我只需要你提供一些基本的几行几列,然后把每个单元格的数据返回给我就行,至于填充数据就不是adapter该做的事了。
然后是读excel,我会把每个单元格的数据都读出来给你,你要怎么组装完全可以自定义。
可以看到adpater不涉及任何poi的类。

  • ValueSetter和ValueGetter
public interface ValueSetter {

    void setValue(int s, int r, int c, Cell cell, Row row, Sheet sheet, Drawing<?> drawing, Workbook workbook, CreationHelper creationHelper, Object value);
}

public interface ValueGetter {

    Object getValue(int s, int r, int c, Cell cell, Row row, Sheet sheet, Drawing<?> drawing, Workbook workbook, CreationHelper creationHelper);
}

ValueSetter的作用是把数据塞到poi里面。
ValueGetter的作用是把数据从poi中读出来。
可以说是数据和view的一层连接。

  • ValueConverter
public interface ValueConverter extends SupportOrder, SupportCache {

    boolean supportValue(int sheet, int row, int cell, Object value);

    Object convertValue(int sheet, int row, int cell, Object value);
}

poi只支持一些有限的数据类型,所以我们还是需要一些转换器来将我们复杂的数据类型转换成poi可以设置读取的类型,甚至包括一个单元格内同时有填充的数据,图片,注释等肯定需要一些自定义处理。

  • 业务流程方法
private static void transfer(Workbook workbook, WriteAdapter writeAdapter, List<PoiListener> poiListeners,
                                 List<ValueConverter> valueConverters, ValueSetter valueSetter) {
    CreationHelper creationHelper = workbook.getCreationHelper();
    int sheetCount = writeAdapter.getSheetCount();
    for (int s = 0; s < sheetCount; s++) {
        String sheetName = writeAdapter.getSheetName(s);
        Sheet sheet;
        if (sheetName == null) {
            sheet = workbook.createSheet();
        } else {
            sheet = workbook.createSheet(sheetName);
        }
        Drawing<?> drawing = sheet.createDrawingPatriarch();
        int rowCount = writeAdapter.getRowCount(s);
        for (int r = 0; r < rowCount; r++) {
            Row row = sheet.createRow(r);
            int cellCount = writeAdapter.getCellCount(s, r);
            for (int c = 0; c < cellCount; c++) {
                Cell cell = row.createCell(c);
                Object o = writeAdapter.getData(s, r, c);
                Object value = convertValue(valueConverters, s, r, c, o);
                valueSetter.setValue(s, r, c, cell, row, sheet, drawing, workbook, creationHelper, value);
            }
        }
    }
  }

private static Object analyze(Workbook workbook, ReadAdapter readAdapter, List<PoiListener> poiListeners,
                                  List<ValueConverter> valueConverters, ValueGetter valueGetter, boolean close) throws IOException {
    CreationHelper creationHelper = workbook.getCreationHelper(); 
    int sCount = workbook.getNumberOfSheets();
    for (int s = 0; s < sCount; s++) {
        Sheet sheet = workbook.getSheetAt(s);
        Drawing<?> drawing = sheet.getDrawingPatriarch();
        int rCount = sheet.getLastRowNum() + 1;
        for (int r = 0; r < rCount; r++) {
            Row row = sheet.getRow(r);
            int cCount = row.getLastCellNum();
            for (int c = 0; c < cCount; c++) {
                Cell cell = row.getCell(c);
                Object o = valueGetter.getValue(s, r, c, cell, row, sheet, drawing, workbook, creationHelper);
                Object cellValue = convertValue(valueConverters, s, r, c, o);
                readAdapter.readCell(cellValue, s, r, c, sCount, rCount, cCount);
            } 
        }
    }
    return readAdapter.getValue();
}

上面就是写和读对应的一个主要逻辑代码(部分删减),我就不做过多的解读了,应该都能看懂。
当然实际上这个库还有很多的一个适配,上述代码只是一个主要的逻辑思想。
我的项目现在用的就是我自己封装的这个库,主要是有bug容易改,再一个就是我可以说自定义Adapter加上ValueConverter相当于可以适配所有的数据结构和特殊情况了。

用法说明

目前这个库里我其实扩展了一些adapter可以直接使用,比如支持List<Bean>+Bean上注解的通用方式实现导出,或者是一个单元格内同时写入或读取数据,图片,注释的支持,或是自适应列宽等,下面罗列了一些具体的基本用法。

依赖
implementation 'com.github.linyuzai:jpoi:0.1.0'
基本用法
JExcel.xlsx().setWriteAdapter(WriteAdapter).write().to(File/OutputStream);//写

Object v = JExcel.xlsx(InputStream).setReadAdapter(ReadAdapter).read().getValue();//读
支持用法
JExcel.xlsx().data(List...).write().to(File/OutputStream);//写类

List<List<Class>> v = JExcel.xlsx(InputStream).target(Class).read().getValue();//读类

List<List<Map<String, Object>>> v = JExcel.xlsx(InputStream).toMap().read().getValue();//读map

List<List<List<Object>>> v = JExcel.xlsx(InputStream).direct().read().getValue();//读list
全部用法
JExcel
      .xls()//HSSFWorkbook
      .xlsx()//XSSFWorkbook
      .sxlsx()//SXSSFWorkbook
      .data()//list bean
      .setWriteAdapter()//自定义WriteAdapter
      .addPoiListener()//poi监听器
      .addValueConverter()//value转换器
      .setValueSetter()//value写入poi的支持类
      .write()//执行写入
      .to();//输出

JExcel
      .xls(InputStream)//HSSFWorkbook
      .xlsx(InputStream)//XSSFWorkbook
      .sxlsx(InputStream)//自定义Sax写法,暂不完善,无法使用
      .target()//转成bean
      .toMap()//转成map
      .direct()//转成list
      .setReadAdapter()//自定义ReadAdapter
      .addPoiListener()//poi监听器
      .addValueConverter()//value转换器
      .setValueGetter()//poi读取value的支持类
      .read()//执行读取
      .getValue();//获得值
注解加类写
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JExcelSheetWriter {
    /**
     * @return sheet名称
     */
    String name() default "";

    /**
     * @return 是否只处理添加了注解的字段
     */
    @Deprecated
    boolean annotationOnly() default true;

    /**
     * @return 样式
     */
    JExcelRowStyle style() default @JExcelRowStyle;
}
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JExcelCellWriter {
    /**
     * @return 每一列的标题
     */
    String title() default "";

    /**
     * @return value转换器
     */
    Class<? extends ValueConverter> valueConverter() default ValueConverter.class;

    /**
     * @return 是否自适应列宽
     */
    boolean autoSize() default true;

    /**
     * @return 指定宽度
     */
    int width() default 0;

    /**
     * @return 排序
     */
    int order() default Integer.MAX_VALUE;

    /**
     * @return 作为对应字段的注释
     */
    String commentOfField() default "";

    /**
     * @return 作为对应index列的注释
     */
    int commentOfIndex() default -1;

    /**
     * @return 作为对应字段的图片
     */
    String pictureOfFiled() default "";

    /**
     * @return 作为对应index的图片
     */
    int pictureOfIndex() default -1;

    @Deprecated
    String standbyFor() default "";

    /**
     * @return 样式
     */
    JExcelCellStyle style() default @JExcelCellStyle;
}
JExcel.xlsx().data(List...).write().to(File/OutputStream);//写类
注解加类读
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JExcelSheetReader {

    @Deprecated
    String name() default "";

    /**
     * @return 是否只处理添加了注解的字段
     */
    @Deprecated
    boolean annotationOnly() default true;

    /**
     * @return 是否转成map
     */
    boolean toMap() default false;
}
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JExcelCellReader {
    /**
     * @return 每一列的标题
     */
    String title() default "";

    /**
     * @return value转换器
     */
    Class<? extends ValueConverter> valueConverter() default ValueConverter.class;

    @Deprecated
    int index() default -1;

    /**
     * @return 作为对应字段的注释
     */
    String commentOfField() default "";

    /**
     * @return 作为对应index列的注释
     */
    int commentOfIndex() default -1;

    /**
     * @return 作为对应字段的图片
     */
    String pictureOfFiled() default "";

    /**
     * @return 作为对应index的图片
     */
    int pictureOfIndex() default -1;
}
List<List<Class>> v = JExcel.xlsx(InputStream).target(Class).read().getValue();//读类
注意事项
  • commentOfField commentOfIndex pictureOfFiled pictureOfIndex只用于支持一个单元格内的多个数据(内容,注释,图片)对应bean的多个字段,如果只想写入一个值或读取一个值,请直接设置ValueConverter

结束

最后感谢您的阅读,如果有什么更好的建议或是使用中有任何问题可以直接提issue,并且我也会持续维护这个项目。另外提一点,如果数据量真的很大的情况下,本人还是建议使用阿里的EasyExcel,对于内存占用做了很大的优化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275