前言
? ? ? ?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,對于內存占用做了很大的優化。