浏览代码

add reader

Looly 5 年之前
父节点
当前提交
50134d8822

+ 1 - 0
CHANGELOG.md

@@ -23,6 +23,7 @@
 * 【http   】     完善StrUtil的注释(pr#186@Gitee)
 * 【aop    】     去除调试日志(issue#1116@Github)
 * 【core   】     增加'反转义(pr#1121@Github)
+* 【poi    】     增加SheetReader和XXXRowHandler(issue#I1WHJP@Gitee)
 
 ### Bug修复
 * 【crypto 】     修复SM2验签后无法解密问题(issue#I1W0VP@Gitee)

+ 21 - 3
hutool-cron/src/test/java/cn/hutool/cron/demo/CronTest.java

@@ -1,12 +1,13 @@
 package cn.hutool.cron.demo;
 
-import org.junit.Ignore;
-import org.junit.Test;
-
 import cn.hutool.core.lang.Console;
 import cn.hutool.core.thread.ThreadUtil;
 import cn.hutool.cron.CronUtil;
+import cn.hutool.cron.TaskExecutor;
+import cn.hutool.cron.listener.TaskListener;
 import cn.hutool.cron.task.Task;
+import org.junit.Ignore;
+import org.junit.Test;
 
 /**
  * 定时任务样例
@@ -38,6 +39,23 @@ public class CronTest {
 	@Test
 	@Ignore
 	public void cronTest2() {
+		CronUtil.getScheduler().addListener(new TaskListener() {
+			@Override
+			public void onStart(TaskExecutor executor) {
+				Console.log("Listen task start!");
+			}
+
+			@Override
+			public void onSucceeded(TaskExecutor executor) {
+
+			}
+
+			@Override
+			public void onFailed(TaskExecutor executor, Throwable exception) {
+
+			}
+		});
+
 		// 支持秒级别定时任务
 		CronUtil.setMatchSecond(true);
 		CronUtil.start();

+ 45 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelExtractorUtil.java

@@ -0,0 +1,45 @@
+package cn.hutool.poi.excel;
+
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.extractor.ExcelExtractor;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.extractor.XSSFExcelExtractor;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+/**
+ * {@link ExcelExtractor}工具封装
+ *
+ * @author looly
+ * @since 5.4.4
+ */
+public class ExcelExtractorUtil {
+	/**
+	 * 获取 {@link ExcelExtractor} 对象
+	 *
+	 * @return {@link ExcelExtractor}
+	 */
+	public static ExcelExtractor getExtractor(Workbook wb) {
+		ExcelExtractor extractor;
+		if (wb instanceof HSSFWorkbook) {
+			extractor = new org.apache.poi.hssf.extractor.ExcelExtractor((HSSFWorkbook) wb);
+		} else {
+			extractor = new XSSFExcelExtractor((XSSFWorkbook) wb);
+		}
+		return extractor;
+	}
+
+	/**
+	 * 读取为文本格式<br>
+	 * 使用{@link ExcelExtractor} 提取Excel内容
+	 *
+	 * @param wb {@link Workbook}
+	 * @param withSheetName 是否附带sheet名
+	 * @return Excel文本
+	 * @since 4.1.0
+	 */
+	public static String readAsText(Workbook wb, boolean withSheetName) {
+		final ExcelExtractor extractor = getExtractor(wb);
+		extractor.setIncludeSheetNames(withSheetName);
+		return extractor.getText();
+	}
+}

+ 46 - 119
hutool-poi/src/main/java/cn/hutool/poi/excel/ExcelReader.java

@@ -1,28 +1,22 @@
 package cn.hutool.poi.excel;
 
-import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.collection.IterUtil;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.lang.Assert;
-import cn.hutool.core.map.MapUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.core.util.StrUtil;
 import cn.hutool.poi.excel.cell.CellEditor;
 import cn.hutool.poi.excel.cell.CellHandler;
 import cn.hutool.poi.excel.cell.CellUtil;
-import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import cn.hutool.poi.excel.reader.BeanSheetReader;
+import cn.hutool.poi.excel.reader.ListSheetReader;
+import cn.hutool.poi.excel.reader.MapSheetReader;
+import cn.hutool.poi.excel.reader.SheetReader;
 import org.apache.poi.ss.extractor.ExcelExtractor;
 import org.apache.poi.ss.usermodel.Cell;
 import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
-import org.apache.poi.xssf.extractor.XSSFExcelExtractor;
-import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 
 import java.io.File;
 import java.io.InputStream;
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -256,37 +250,31 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
 	}
 
 	/**
-	 * 读取工作簿中指定的Sheet
+	 * 读取工作簿中指定的Sheet,此方法会把第一行作为标题行,替换标题别名
 	 *
 	 * @param startRowIndex 起始行(包含,从0开始计数)
 	 * @param endRowIndex   结束行(包含,从0开始计数)
 	 * @return 行的集合,一行使用List表示
 	 */
-	@SuppressWarnings({"rawtypes", "unchecked"})
 	public List<List<Object>> read(int startRowIndex, int endRowIndex) {
-		checkNotClosed();
-		List<List<Object>> resultList = new ArrayList<>();
+		return read(startRowIndex, endRowIndex, true);
+	}
 
-		startRowIndex = Math.max(startRowIndex, this.sheet.getFirstRowNum());// 读取起始行(包含)
-		endRowIndex = Math.min(endRowIndex, this.sheet.getLastRowNum());// 读取结束行(包含)
-		boolean isFirstLine = true;
-		List rowList;
-		for (int i = startRowIndex; i <= endRowIndex; i++) {
-			rowList = readRow(i);
-			if (CollUtil.isNotEmpty(rowList) || false == ignoreEmptyRow) {
-				if (null == rowList) {
-					rowList = new ArrayList<>(0);
-				}
-				if (isFirstLine) {
-					isFirstLine = false;
-					if (MapUtil.isNotEmpty(this.headerAlias)) {
-						rowList = aliasHeader(rowList);
-					}
-				}
-				resultList.add(rowList);
-			}
-		}
-		return resultList;
+	/**
+	 * 读取工作簿中指定的Sheet
+	 *
+	 * @param startRowIndex 起始行(包含,从0开始计数)
+	 * @param endRowIndex   结束行(包含,从0开始计数)
+	 * @param aliasFirstLine 是否首行作为标题行转换别名
+	 * @return 行的集合,一行使用List表示
+	 * @since 5.4.4
+	 */
+	public List<List<Object>> read(int startRowIndex, int endRowIndex, boolean aliasFirstLine) {
+		final ListSheetReader reader = new ListSheetReader(startRowIndex, endRowIndex, aliasFirstLine);
+		reader.setCellEditor(this.cellEditor);
+		reader.setIgnoreEmptyRow(this.ignoreEmptyRow);
+		reader.setHeaderAlias(headerAlias);
+		return read(reader);
 	}
 
 	/**
@@ -348,33 +336,11 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
 	 * @return Map的列表
 	 */
 	public List<Map<String, Object>> read(int headerRowIndex, int startRowIndex, int endRowIndex) {
-		checkNotClosed();
-		// 边界判断
-		final int firstRowNum = sheet.getFirstRowNum();
-		final int lastRowNum = sheet.getLastRowNum();
-		if (headerRowIndex < firstRowNum) {
-			throw new IndexOutOfBoundsException(StrUtil.format("Header row index {} is lower than first row index {}.", headerRowIndex, firstRowNum));
-		} else if (headerRowIndex > lastRowNum) {
-			throw new IndexOutOfBoundsException(StrUtil.format("Header row index {} is greater than last row index {}.", headerRowIndex, firstRowNum));
-		}
-		startRowIndex = Math.max(startRowIndex, firstRowNum);// 读取起始行(包含)
-		endRowIndex = Math.min(endRowIndex, lastRowNum);// 读取结束行(包含)
-
-		// 读取header
-		List<Object> headerList = readRow(sheet.getRow(headerRowIndex));
-
-		final List<Map<String, Object>> result = new ArrayList<>(endRowIndex - startRowIndex + 1);
-		List<Object> rowList;
-		for (int i = startRowIndex; i <= endRowIndex; i++) {
-			if (i != headerRowIndex) {
-				// 跳过标题行
-				rowList = readRow(sheet.getRow(i));
-				if (CollUtil.isNotEmpty(rowList) || false == ignoreEmptyRow) {
-					result.add(IterUtil.toMap(aliasHeader(headerList), rowList, true));
-				}
-			}
-		}
-		return result;
+		final MapSheetReader reader = new MapSheetReader(headerRowIndex, startRowIndex, endRowIndex);
+		reader.setCellEditor(this.cellEditor);
+		reader.setIgnoreEmptyRow(this.ignoreEmptyRow);
+		reader.setHeaderAlias(headerAlias);
+		return read(reader);
 	}
 
 	/**
@@ -412,19 +378,25 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
 	 * @param beanType       每行对应Bean的类型
 	 * @return Map的列表
 	 */
-	@SuppressWarnings("unchecked")
 	public <T> List<T> read(int headerRowIndex, int startRowIndex, int endRowIndex, Class<T> beanType) {
-		checkNotClosed();
-		final List<Map<String, Object>> mapList = read(headerRowIndex, startRowIndex, endRowIndex);
-		if (Map.class.isAssignableFrom(beanType)) {
-			return (List<T>) mapList;
-		}
+		final BeanSheetReader<T> reader = new BeanSheetReader<>(headerRowIndex, startRowIndex, endRowIndex, beanType);
+		reader.setCellEditor(this.cellEditor);
+		reader.setIgnoreEmptyRow(this.ignoreEmptyRow);
+		reader.setHeaderAlias(headerAlias);
+		return read(reader);
+	}
 
-		final List<T> beanList = new ArrayList<>(mapList.size());
-		for (Map<String, Object> map : mapList) {
-			beanList.add(BeanUtil.toBean(map, beanType));
-		}
-		return beanList;
+	/**
+	 * 读取数据为指定类型
+	 *
+	 * @param <T> 读取数据类型
+	 * @param sheetReader {@link SheetReader}实现
+	 * @return 数据读取结果
+	 * @since 5.4.4
+	 */
+	public <T> T read(SheetReader<T> sheetReader){
+		checkNotClosed();
+		return Assert.notNull(sheetReader).read(this.sheet);
 	}
 
 	/**
@@ -436,9 +408,7 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
 	 * @since 4.1.0
 	 */
 	public String readAsText(boolean withSheetName) {
-		final ExcelExtractor extractor = getExtractor();
-		extractor.setIncludeSheetNames(withSheetName);
-		return extractor.getText();
+		return ExcelExtractorUtil.readAsText(this.workbook, withSheetName);
 	}
 
 	/**
@@ -448,14 +418,7 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
 	 * @since 4.1.0
 	 */
 	public ExcelExtractor getExtractor() {
-		ExcelExtractor extractor;
-		Workbook wb = this.workbook;
-		if (wb instanceof HSSFWorkbook) {
-			extractor = new org.apache.poi.hssf.extractor.ExcelExtractor((HSSFWorkbook) wb);
-		} else {
-			extractor = new XSSFExcelExtractor((XSSFWorkbook) wb);
-		}
-		return extractor;
+		return ExcelExtractorUtil.getExtractor(this.workbook);
 	}
 
 	/**
@@ -505,42 +468,6 @@ public class ExcelReader extends ExcelBase<ExcelReader> {
 	}
 
 	/**
-	 * 转换标题别名,如果没有别名则使用原标题,当标题为空时,列号对应的字母便是header
-	 *
-	 * @param headerList 原标题列表
-	 * @return 转换别名列表
-	 */
-	private List<String> aliasHeader(List<Object> headerList) {
-		if(CollUtil.isEmpty(headerList)){
-			return new ArrayList<>(0);
-		}
-
-		final int size = headerList.size();
-		final ArrayList<String> result = new ArrayList<>(size);
-		for (int i = 0; i < size; i++) {
-			result.add(aliasHeader(headerList.get(i), i));
-		}
-		return result;
-	}
-
-	/**
-	 * 转换标题别名,如果没有别名则使用原标题,当标题为空时,列号对应的字母便是header
-	 *
-	 * @param headerObj 原标题
-	 * @param index     标题所在列号,当标题为空时,列号对应的字母便是header
-	 * @return 转换别名列表
-	 * @since 4.3.2
-	 */
-	private String aliasHeader(Object headerObj, int index) {
-		if (null == headerObj) {
-			return ExcelUtil.indexToColName(index);
-		}
-
-		final String header = headerObj.toString();
-		return ObjectUtil.defaultIfNull(this.headerAlias.get(header), header);
-	}
-
-	/**
 	 * 检查是否未关闭状态
 	 */
 	private void checkNotClosed() {

+ 140 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/reader/AbstractSheetReader.java

@@ -0,0 +1,140 @@
+package cn.hutool.poi.excel.reader;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.poi.excel.ExcelUtil;
+import cn.hutool.poi.excel.RowUtil;
+import cn.hutool.poi.excel.cell.CellEditor;
+import org.apache.poi.ss.usermodel.Sheet;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 抽象{@link Sheet}数据读取实现
+ *
+ * @param <T> 读取类型
+ * @author looly
+ * @since 5.4.4
+ */
+public abstract class AbstractSheetReader<T> implements SheetReader<T> {
+
+	/**
+	 * 读取起始行(包含,从0开始计数)
+	 */
+	protected final int startRowIndex;
+	/**
+	 * 读取结束行(包含,从0开始计数)
+	 */
+	protected final int endRowIndex;
+	/**
+	 * 是否忽略空行
+	 */
+	protected boolean ignoreEmptyRow = true;
+	/**
+	 * 单元格值处理接口
+	 */
+	protected CellEditor cellEditor;
+	/**
+	 * 标题别名
+	 */
+	private Map<String, String> headerAlias = new HashMap<>();
+
+	/**
+	 * 构造
+	 *
+	 * @param startRowIndex 起始行(包含,从0开始计数)
+	 * @param endRowIndex   结束行(包含,从0开始计数)
+	 */
+	public AbstractSheetReader(int startRowIndex, int endRowIndex) {
+		this.startRowIndex = startRowIndex;
+		this.endRowIndex = endRowIndex;
+	}
+
+	/**
+	 * 设置单元格值处理逻辑<br>
+	 * 当Excel中的值并不能满足我们的读取要求时,通过传入一个编辑接口,可以对单元格值自定义,例如对数字和日期类型值转换为字符串等
+	 *
+	 * @param cellEditor 单元格值处理接口
+	 */
+	public void setCellEditor(CellEditor cellEditor) {
+		this.cellEditor = cellEditor;
+	}
+
+	/**
+	 * 设置是否忽略空行
+	 *
+	 * @param ignoreEmptyRow 是否忽略空行
+	 */
+	public void setIgnoreEmptyRow(boolean ignoreEmptyRow) {
+		this.ignoreEmptyRow = ignoreEmptyRow;
+	}
+
+	/**
+	 * 设置标题行的别名Map
+	 *
+	 * @param headerAlias 别名Map
+	 */
+	public void setHeaderAlias(Map<String, String> headerAlias) {
+		this.headerAlias = headerAlias;
+	}
+
+	/**
+	 * 增加标题别名
+	 *
+	 * @param header 标题
+	 * @param alias  别名
+	 */
+	public void addHeaderAlias(String header, String alias) {
+		this.headerAlias.put(header, alias);
+	}
+
+	/**
+	 * 转换标题别名,如果没有别名则使用原标题,当标题为空时,列号对应的字母便是header
+	 *
+	 * @param headerList 原标题列表
+	 * @return 转换别名列表
+	 */
+	protected List<String> aliasHeader(List<Object> headerList) {
+		if (CollUtil.isEmpty(headerList)) {
+			return new ArrayList<>(0);
+		}
+
+		final int size = headerList.size();
+		final ArrayList<String> result = new ArrayList<>(size);
+		for (int i = 0; i < size; i++) {
+			result.add(aliasHeader(headerList.get(i), i));
+		}
+		return result;
+	}
+
+	/**
+	 * 转换标题别名,如果没有别名则使用原标题,当标题为空时,列号对应的字母便是header
+	 *
+	 * @param headerObj 原标题
+	 * @param index     标题所在列号,当标题为空时,列号对应的字母便是header
+	 * @return 转换别名列表
+	 * @since 4.3.2
+	 */
+	protected String aliasHeader(Object headerObj, int index) {
+		if (null == headerObj) {
+			return ExcelUtil.indexToColName(index);
+		}
+
+		final String header = headerObj.toString();
+		return ObjectUtil.defaultIfNull(this.headerAlias.get(header), header);
+	}
+
+	/**
+	 * 读取某一行数据
+	 *
+	 * @param rowIndex 行号,从0开始
+	 * @return 一行数据
+	 * @since 4.0.3
+	 */
+	protected List<Object> readRow(Sheet sheet, int rowIndex) {
+		return RowUtil.readRow(sheet.getRow(rowIndex), this.cellEditor);
+	}
+}

+ 87 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/reader/BeanSheetReader.java

@@ -0,0 +1,87 @@
+package cn.hutool.poi.excel.reader;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.poi.excel.cell.CellEditor;
+import org.apache.poi.ss.usermodel.Sheet;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 读取{@link Sheet}为bean的List列表形式
+ *
+ * @author looly
+ * @since 5.4.4
+ */
+public class BeanSheetReader<T> implements SheetReader<List<T>> {
+
+	private final Class<T> beanClass;
+	private final MapSheetReader mapSheetReader;
+
+	/**
+	 * 构造
+	 *
+	 * @param headerRowIndex 标题所在行,如果标题行在读取的内容行中间,这行做为数据将忽略
+	 * @param startRowIndex  起始行(包含,从0开始计数)
+	 * @param endRowIndex    结束行(包含,从0开始计数)
+	 * @param beanClass      每行对应Bean的类型
+	 */
+	public BeanSheetReader(int headerRowIndex, int startRowIndex, int endRowIndex, Class<T> beanClass) {
+		mapSheetReader = new MapSheetReader(headerRowIndex, startRowIndex, endRowIndex);
+		this.beanClass = beanClass;
+	}
+
+	@Override
+	@SuppressWarnings("unchecked")
+	public List<T> read(Sheet sheet) {
+		final List<Map<String, Object>> mapList = mapSheetReader.read(sheet);
+		if (Map.class.isAssignableFrom(this.beanClass)) {
+			return (List<T>) mapList;
+		}
+
+		final List<T> beanList = new ArrayList<>(mapList.size());
+		for (Map<String, Object> map : mapList) {
+			beanList.add(BeanUtil.toBean(map, this.beanClass));
+		}
+		return beanList;
+	}
+
+	/**
+	 * 设置单元格值处理逻辑<br>
+	 * 当Excel中的值并不能满足我们的读取要求时,通过传入一个编辑接口,可以对单元格值自定义,例如对数字和日期类型值转换为字符串等
+	 *
+	 * @param cellEditor 单元格值处理接口
+	 */
+	public void setCellEditor(CellEditor cellEditor) {
+		this.mapSheetReader.setCellEditor(cellEditor);
+	}
+
+	/**
+	 * 设置是否忽略空行
+	 *
+	 * @param ignoreEmptyRow 是否忽略空行
+	 */
+	public void setIgnoreEmptyRow(boolean ignoreEmptyRow) {
+		this.mapSheetReader.setIgnoreEmptyRow(ignoreEmptyRow);
+	}
+
+	/**
+	 * 设置标题行的别名Map
+	 *
+	 * @param headerAlias 别名Map
+	 */
+	public void setHeaderAlias(Map<String, String> headerAlias) {
+		this.mapSheetReader.setHeaderAlias(headerAlias);
+	}
+
+	/**
+	 * 增加标题别名
+	 *
+	 * @param header 标题
+	 * @param alias  别名
+	 */
+	public void addHeaderAlias(String header, String alias) {
+		this.mapSheetReader.addHeaderAlias(header, alias);
+	}
+}

+ 50 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/reader/ListSheetReader.java

@@ -0,0 +1,50 @@
+package cn.hutool.poi.excel.reader;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
+import org.apache.poi.ss.usermodel.Sheet;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 读取{@link Sheet}为List列表形式
+ *
+ * @author looly
+ * @since 5.4.4
+ */
+public class ListSheetReader extends AbstractSheetReader<List<List<Object>>> {
+
+	private final boolean aliasFirstLine;
+
+	/**
+	 * 构造
+	 *
+	 * @param startRowIndex 起始行(包含,从0开始计数)
+	 * @param endRowIndex   结束行(包含,从0开始计数)
+	 */
+	public ListSheetReader(int startRowIndex, int endRowIndex, boolean aliasFirstLine) {
+		super(startRowIndex, endRowIndex);
+		this.aliasFirstLine = aliasFirstLine;
+	}
+
+	@Override
+	public List<List<Object>> read(Sheet sheet) {
+		final List<List<Object>> resultList = new ArrayList<>();
+
+		int startRowIndex = Math.max(this.startRowIndex, sheet.getFirstRowNum());// 读取起始行(包含)
+		int endRowIndex = Math.min(this.endRowIndex, sheet.getLastRowNum());// 读取结束行(包含)
+		List<Object> rowList;
+		for (int i = startRowIndex; i <= endRowIndex; i++) {
+			rowList = readRow(sheet, i);
+			if (CollUtil.isNotEmpty(rowList) || false == ignoreEmptyRow) {
+				if (aliasFirstLine && i == startRowIndex) {
+					// 第一行作为标题行,替换别名
+					rowList = Convert.toList(Object.class, aliasHeader(rowList));
+				}
+				resultList.add(rowList);
+			}
+		}
+		return resultList;
+	}
+}

+ 63 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/reader/MapSheetReader.java

@@ -0,0 +1,63 @@
+package cn.hutool.poi.excel.reader;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.IterUtil;
+import cn.hutool.core.util.StrUtil;
+import org.apache.poi.ss.usermodel.Sheet;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 读取{@link Sheet}为Map的List列表形式
+ *
+ * @author looly
+ * @since 5.4.4
+ */
+public class MapSheetReader extends AbstractSheetReader<List<Map<String, Object>>> {
+
+	private final int headerRowIndex;
+
+	/**
+	 * 构造
+	 *
+	 * @param headerRowIndex 标题所在行,如果标题行在读取的内容行中间,这行做为数据将忽略
+	 * @param startRowIndex 起始行(包含,从0开始计数)
+	 * @param endRowIndex   结束行(包含,从0开始计数)
+	 */
+	public MapSheetReader(int headerRowIndex, int startRowIndex, int endRowIndex) {
+		super(startRowIndex, endRowIndex);
+		this.headerRowIndex = headerRowIndex;
+	}
+
+	@Override
+	public List<Map<String, Object>> read(Sheet sheet) {
+		// 边界判断
+		final int firstRowNum = sheet.getFirstRowNum();
+		final int lastRowNum = sheet.getLastRowNum();
+		if (headerRowIndex < firstRowNum) {
+			throw new IndexOutOfBoundsException(StrUtil.format("Header row index {} is lower than first row index {}.", headerRowIndex, firstRowNum));
+		} else if (headerRowIndex > lastRowNum) {
+			throw new IndexOutOfBoundsException(StrUtil.format("Header row index {} is greater than last row index {}.", headerRowIndex, firstRowNum));
+		}
+		final int startRowIndex = Math.max(this.startRowIndex, firstRowNum);// 读取起始行(包含)
+		final int endRowIndex = Math.min(this.endRowIndex, lastRowNum);// 读取结束行(包含)
+
+		// 读取header
+		List<String> headerList = aliasHeader(readRow(sheet, headerRowIndex));
+
+		final List<Map<String, Object>> result = new ArrayList<>(endRowIndex - startRowIndex + 1);
+		List<Object> rowList;
+		for (int i = startRowIndex; i <= endRowIndex; i++) {
+			// 跳过标题行
+			if (i != headerRowIndex) {
+				rowList = readRow(sheet, i);
+				if (CollUtil.isNotEmpty(rowList) || false == ignoreEmptyRow) {
+					result.add(IterUtil.toMap(headerList, rowList, true));
+				}
+			}
+		}
+		return result;
+	}
+}

+ 20 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/reader/SheetReader.java

@@ -0,0 +1,20 @@
+package cn.hutool.poi.excel.reader;
+
+import org.apache.poi.ss.usermodel.Sheet;
+
+/**
+ * Excel {@link Sheet}读取接口,通过实现此接口,将{@link Sheet}中的数据读取为不同类型。
+ *
+ * @param <T> 读取的数据类型
+ */
+@FunctionalInterface
+public interface SheetReader<T> {
+
+	/**
+	 * 读取数据
+	 *
+	 * @param sheet {@link Sheet}
+	 * @return 读取结果
+	 */
+	T read(Sheet sheet);
+}

+ 7 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/reader/package-info.java

@@ -0,0 +1,7 @@
+/**
+ * 数据读取接口及实现,此包中定义了SheetReader,通过实现此接口,实现sheet中的数据读取为不同类型。
+ * 
+ * @author looly
+ *
+ */
+package cn.hutool.poi.excel.reader;

+ 59 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/AbstractRowHandler.java

@@ -0,0 +1,59 @@
+package cn.hutool.poi.excel.sax.handler;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.lang.func.Func1;
+
+import java.util.List;
+
+/**
+ * 抽象行数据处理器,通过实现{@link #handle(int, long, List)} 处理原始数据<br>
+ * 并调用{@link #handleData(int, long, Object)}处理经过转换后的数据。
+ *
+ * @param <T> 转换后的数据类型
+ * @author looly
+ * @since 5.4.4
+ */
+public abstract class AbstractRowHandler<T> implements RowHandler {
+
+	/**
+	 * 读取起始行(包含,从0开始计数)
+	 */
+	protected final int startRowIndex;
+	/**
+	 * 读取结束行(包含,从0开始计数)
+	 */
+	protected final int endRowIndex;
+	/**
+	 * 行数据转换函数
+	 */
+	protected Func1<List<Object>, T> convertFunc;
+
+	/**
+	 * 构造
+	 *
+	 * @param startRowIndex 读取起始行(包含,从0开始计数)
+	 * @param endRowIndex 读取结束行(包含,从0开始计数)
+	 */
+	public AbstractRowHandler(int startRowIndex, int endRowIndex) {
+		this.startRowIndex = startRowIndex;
+		this.endRowIndex = endRowIndex;
+	}
+
+	@Override
+	public void handle(int sheetIndex, long rowIndex, List<Object> rowList) {
+		Assert.notNull(convertFunc);
+		if (rowIndex < this.startRowIndex || rowIndex > this.endRowIndex) {
+			return;
+		}
+		handleData(sheetIndex, rowIndex, convertFunc.callWithRuntimeException(rowList));
+	}
+
+	/**
+	 * 处理转换后的数据
+	 *
+	 * @param sheetIndex 当前Sheet序号
+	 * @param rowIndex   当前行号,从0开始计数
+	 * @param data       行数据
+	 */
+	public abstract void handleData(int sheetIndex, long rowIndex, T data);
+}

+ 51 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/BeanRowHandler.java

@@ -0,0 +1,51 @@
+package cn.hutool.poi.excel.sax.handler;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.IterUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.lang.Assert;
+
+import java.util.List;
+
+/**
+ * Bean形式的行处理器<br>
+ * 将一行数据转换为Map,key为指定行,value为当前行对应位置的值
+ *
+ * @author looly
+ * @since 5.4.4
+ */
+public abstract class BeanRowHandler<T> extends AbstractRowHandler<T>{
+
+	/**
+	 * 标题所在行(从0开始计数)
+	 */
+	private final int headerRowIndex;
+	/**
+	 * 标题行
+	 */
+	List<String> headerList;
+
+	/**
+	 * 构造
+	 *
+	 * @param headerRowIndex 标题所在行(从0开始计数)
+	 * @param startRowIndex 读取起始行(包含,从0开始计数)
+	 * @param endRowIndex 读取结束行(包含,从0开始计数)
+	 */
+	public BeanRowHandler(int headerRowIndex, int startRowIndex, int endRowIndex, Class<T> clazz){
+		super(startRowIndex, endRowIndex);
+		Assert.isTrue(headerRowIndex <= startRowIndex, "Header row must before the start row!");
+		this.headerRowIndex = headerRowIndex;
+		this.convertFunc = (rowList)-> BeanUtil.toBean(IterUtil.toMap(headerList, rowList), clazz);
+	}
+
+	@Override
+	public void handle(int sheetIndex, long rowIndex, List<Object> rowList) {
+		if (rowIndex == this.headerRowIndex) {
+			this.headerList = ListUtil.unmodifiable(Convert.toList(String.class, rowList));
+			return;
+		}
+		super.handle(sheetIndex, rowIndex, rowList);
+	}
+}

+ 49 - 0
hutool-poi/src/main/java/cn/hutool/poi/excel/sax/handler/MapRowHandler.java

@@ -0,0 +1,49 @@
+package cn.hutool.poi.excel.sax.handler;
+
+import cn.hutool.core.collection.IterUtil;
+import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.convert.Convert;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Map形式的行处理器<br>
+ * 将一行数据转换为Map,key为指定行,value为当前行对应位置的值
+ *
+ * @author looly
+ * @since 5.4.4
+ */
+public abstract class MapRowHandler extends AbstractRowHandler<Map<String, Object>> {
+
+	/**
+	 * 标题所在行(从0开始计数)
+	 */
+	private final int headerRowIndex;
+	/**
+	 * 标题行
+	 */
+	List<String> headerList;
+
+	/**
+	 * 构造
+	 *
+	 * @param headerRowIndex 标题所在行(从0开始计数)
+	 * @param startRowIndex 读取起始行(包含,从0开始计数)
+	 * @param endRowIndex 读取结束行(包含,从0开始计数)
+	 */
+	public MapRowHandler(int headerRowIndex, int startRowIndex, int endRowIndex){
+		super(startRowIndex, endRowIndex);
+		this.headerRowIndex = headerRowIndex;
+		this.convertFunc = (rowList)-> IterUtil.toMap(headerList, rowList);
+	}
+
+	@Override
+	public void handle(int sheetIndex, long rowIndex, List<Object> rowList) {
+		if (rowIndex == this.headerRowIndex) {
+			this.headerList = ListUtil.unmodifiable(Convert.toList(String.class, rowList));
+			return;
+		}
+		super.handle(sheetIndex, rowIndex, rowList);
+	}
+}