前言
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,对于内存占用做了很大的优化。