該文章為本系列的第四篇
第一篇為 : Java POI操作Excel(User Model)
第二篇為 : Java POI操作Excel(Event Model)
第三篇為 : Java POI操作Excel(Event User Model)
前言
通過前面的三篇文章,我們已經(jīng)對POI解析Excel有了不錯(cuò)的理解.這篇文章,我們就來自己封裝一個(gè)Excel解析框架.
那為什么要自己做一個(gè)解析框架?這個(gè)問題的本質(zhì),我覺得應(yīng)該從個(gè)人的商業(yè)模式講起.
我們每天去工作,賺取工資,本質(zhì)上是在用我們只去不回的時(shí)間和注意力來換取金錢.那如果我們想提升我們獲取的回報(bào),顯而易見的方式就是提升時(shí)薪.而除此之外,還有一個(gè)升級的辦法,那就是把一份時(shí)間賣出很多份.比如暢銷書的作家,寫一本書.時(shí)間只用了一次,但是卻可以在寫完之后仍然在產(chǎn)生回報(bào).
那作為程序員,我們能否也使用這種思路去解決工作中的問題呢,當(dāng)然可以,比如說,我們今天要做的,封裝一個(gè)Excel解析框架就是這樣一種思路.在我們可預(yù)期的后續(xù)工作中,Excel導(dǎo)入數(shù)據(jù)這種功能肯定是還會(huì)再寫的.但是如果這次寫完,下次遇到我還是去查資料,重新寫.那不僅僅是重復(fù)勞動(dòng).這次遇到的坑,下次可能會(huì)難免再踩一些.而如果我們在這一次封裝了自己的庫.下次再遇到,我們可以直接使用.不僅可以節(jié)約時(shí)間,也不會(huì)踩到同樣的坑.所以,讓我們開始行動(dòng)吧~
分析需求
在我們的工作中,對于Excel上傳,我們會(huì)遇到的場景一般是把上傳來的Excel進(jìn)行解析,組裝成一個(gè)對象,然后校驗(yàn)數(shù)據(jù),轉(zhuǎn)成Po,導(dǎo)入數(shù)據(jù)庫.而這個(gè)流程中,我們的Excel解析框架要做的事情,實(shí)際上就是解析Excel和組裝對象.我們希望我們只用一點(diǎn)點(diǎn)的代碼,就可以把Excel解析完,并且可以自由選擇使用Dom方式解析還是Sax方式.甚至希望可以不知道上傳的Excel的版本.
接口定義
提供解析功能的接口,可以理解為是一個(gè)門面(Facade).
public interface IExcelParser<T> {
List<T> parse(IParserParam parserParam);
}
關(guān)于解析方法的參數(shù)規(guī)范.
上傳的過程中,我們需要Excel的流,要解析完成后組裝的對象的類型,Excel中有多少列的數(shù)據(jù).要解析的Sheet,以及表頭數(shù)據(jù).
由于Excel是外部通過上傳,所以一般情況下,我們會(huì)對表頭數(shù)據(jù)進(jìn)行校驗(yàn).來達(dá)到功能的收斂,防止誤操作,對系統(tǒng)造成影響.當(dāng)然如果不想校驗(yàn),在我們的解析框架中,也應(yīng)該是支持的.
public interface IParserParam {
Integer FIRST_SHEET = 0;
InputStream getExcelInputStream();
Class getTargetClass();
Integer getColumnSize();
Integer getSheetNum();
List<String> getHeader();
}
整體設(shè)計(jì)
IExcelParseHandler接口提供具體的解析服務(wù).對上層的Parser屏蔽解析細(xì)節(jié).
客戶端代碼
我們從調(diào)用端的代碼進(jìn)行分析,來達(dá)到管中規(guī)豹的效果.
@Test
public void testDomXlsx() {
parser = new ExcelDomParser<>();
IParserParam parserParam = DefaultParserParam.builder()
.excelInputStream(Thread.currentThread().getContextClassLoader()
.getResourceAsStream("test01.xlsx"))
.columnSize(4)
.sheetNum(IParserParam.FIRST_SHEET)
.targetClass(User.class)
.header(User.getHeader())
.build();
List<User> user = parser.parse(parserParam);
System.out.println(user);
}
User類:
public class User {
@ExcelField(index = 0)
private String name;
@ExcelField(index = 1)
private String age;
@ExcelField(index = 2)
private String gender;
@ExcelField(index = 3, type = ExcelField.ExcelFieldType.Date)
private String dateStr;
客戶端代碼十分簡單,我們只需要組裝一個(gè)IParserParam的默認(rèn)對象,DefaultParserParam.然后傳入到Parser中即可解析完成.
再看看User類.User類的字段上出現(xiàn)了ExcelField注解.我們都知道要想把一行數(shù)據(jù)轉(zhuǎn)成對象,使用反射是最簡單的方式,所以ExcelField就是對應(yīng)字段和在Excel中的列數(shù)使用.
至于為什么字段都定義為String,因?yàn)楹罄m(xù)還要轉(zhuǎn)對象為Po.在Excel上傳解析這個(gè)地方使用String類型最為方便.
線程安全問題
在Web項(xiàng)目中使用我們的框架,必然是要與Spring進(jìn)行整合.在整合的時(shí)候Spring會(huì)默認(rèn)給我們創(chuàng)建單例的解析類.而我們要做的就是保證這個(gè)單例的解析類不會(huì)存在線程安全問題.那這是怎么實(shí)現(xiàn)的呢.
我們先來看下dom解析的方式
public class ExcelDomParser<T> extends AbstractExcelParser<T> {
private IExcelParseHandler<T> excelParseHandler;
public ExcelDomParser() {
this.excelParseHandler = new ExcelDomParseHandler<>();
}
@Override
protected IExcelParseHandler<T> createHandler(InputStream excelInputStream) {
return this.excelParseHandler;
}
}
上面是上層DomParser的代碼,根據(jù)代碼我們可以發(fā)現(xiàn),excelParseHandler是成員變量.一直都是使用的一個(gè).那接下來我們再看一下DomparseHandler的實(shí)現(xiàn).
public class ExcelDomParseHandler<T> extends BaseExcelParseHandler<T> {
@Override
public List<T> process(IParserParam parserParam) throws Exception {
Workbook workbook = generateWorkBook(parserParam);
Sheet sheet = workbook.getSheetAt(parserParam.getSheetNum());
Iterator<Row> rowIterator = sheet.rowIterator();
if (parserParam.getHeader() != null && parserParam.getHeader().size() != 0) {
checkHeader(rowIterator, parserParam);
}
return parseRowToTargetList(rowIterator, parserParam);
}
private void checkHeader(Iterator<Row> rowIterator, IParserParam parserParam) {
while (true) {
Row row = rowIterator.next();
List<String> rowData = parseRowToList(row, parserParam.getColumnSize());
boolean empty = isRowDataEmpty(rowData);
if (!empty) {
validHeader(parserParam, rowData);
break;
}
}
}
private Workbook generateWorkBook(IParserParam parserParam) throws IOException, InvalidFormatException {
return WorkbookFactory.create(parserParam.getExcelInputStream());
}
private List<T> parseRowToTargetList(Iterator<Row> rowIterator, IParserParam parserParam) throws InstantiationException, IllegalAccessException {
List<T> result = new ArrayList<>();
for (; rowIterator.hasNext(); ) {
Row row = rowIterator.next();
List<String> rowData = parseRowToList(row, parserParam.getColumnSize());
Optional<T> d = parseRowToTarget(parserParam, rowData);
d.ifPresent(result::add);
}
return result;
}
private List<String> parseRowToList(Row row, int size) {
List<String> dataRow = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
if (row.getCell(i) != null) {
DataFormatter formatter = new DataFormatter();
String formattedCellValue = formatter.formatCellValue(row.getCell(i));
dataRow.add(formattedCellValue.trim());
} else {
dataRow.add("");
}
}
return dataRow;
}
}
我們通過代碼看到DomParseHandler本身沒有使用任何的成員變量,而父類BaseExcelParseHandler中存在的一個(gè)成員變量head,也沒有在這個(gè)類中使用.所以這個(gè)類在多線程環(huán)境下是安全的.不會(huì)存在問題.
接下來我們看一下Sax解析的Parser
public class ExcelSaxParser<T> extends AbstractExcelParser<T> {
public IExcelParseHandler<T> createHandler(InputStream excelInputStream) {
try {
byte[] header8 = IOUtils.peekFirst8Bytes(excelInputStream);
if (NPOIFSFileSystem.hasPOIFSHeader(header8)) {
return new Excel2003ParseHandler<>();
} else if (DocumentFactoryHelper.hasOOXMLHeader(excelInputStream)) {
return new Excel2007ParseHandler<>();
} else {
throw new IllegalArgumentException("Your InputStream was neither an OLE2 stream, nor an OOXML stream");
}
} catch (Exception e) {
logger.error("getParserInstance Error!", e);
throw new RuntimeException(e);
}
}
}
通過代碼,我們發(fā)現(xiàn),每次都會(huì)創(chuàng)建一個(gè)新的Handler,并且根據(jù)不同判斷使用不同的Handler.這種方式在多線程環(huán)境下也不會(huì)存在問題.可以使用Spring的單例進(jìn)行管理
與Spring整合
使用Dom方式
<bean id = "excelParser" class="com.snakotech.excelhelper.ExcelDomparser">
@Autowire
private IExcelParser excelParser;
使用Sax方式
<bean id = "excelParser" class="com.snakotech.excelhelper.ExcelSaxparser">
@Autowire
private IExcelParser excelParser;
總結(jié)
由于代碼比較多,所以不能面面俱到的講解所有的細(xì)節(jié),但是看完整篇文章,相信你對如何封裝也有了一定的想法,可以去嘗試著實(shí)現(xiàn)屬于你自己的Excel解析框架.在做的過程中,相信你一定獲益匪淺