Looly 6 年 前
コミット
0553c5ca0b

+ 6 - 0
CHANGELOG.md

@@ -7,9 +7,15 @@
 
 ### 新特性
 * 【poi  】     增加单元格位置引用(例如A11等方式获取单元格)
+* 【extra】     ServletUtil.fillBean支持数据和集合字段(issue#I19ZMK@Gitee)
+* 【core 】     修改ThreadUtil.newSingleExecutor默认队列大小(issue#754@Github)
+* 【core 】     修改ExecutorBuilder默认队列大小(issue#753@Github)
+* 【core 】     FileTypeUtil增加mp4的magic(issue#756@Github)
 
 ### Bug修复
 * 【core 】     修复CombinationAnnotationElement数组判断问题(issue#752@Github)
+* 【core 】     修复log4j2使用debug行号打印问题(issue#I19NFJ@Github)
+* 【poi  】     修复sax读取excel03数组越界问题(issue#750@Github)
 
 -------------------------------------------------------------------------------------------------------------
 

+ 2 - 1
hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java

@@ -42,7 +42,8 @@ public class FileTypeUtil {
 		fileTypeMap.put("255044462d312e", "pdf"); // Adobe Acrobat (pdf)
 		fileTypeMap.put("2e524d46000000120001", "rmvb"); // rmvb/rm相同
 		fileTypeMap.put("464c5601050000000900", "flv"); // flv与f4v相同
-		fileTypeMap.put("00000020667479706d70", "mp4");
+		fileTypeMap.put("00000020667479706", "mp4");
+		fileTypeMap.put("00000018667479706D70", "mp4");
 		fileTypeMap.put("49443303000000002176", "mp3");
 		fileTypeMap.put("000001ba210001000180", "mpg"); //
 		fileTypeMap.put("3026b2758e66cf11a6d9", "wmv"); // wmv与asf相同

+ 88 - 0
hutool-core/src/main/java/cn/hutool/core/thread/DelegatedExecutorService.java

@@ -0,0 +1,88 @@
+package cn.hutool.core.thread;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * ExecutorService代理
+ *
+ * @author loolly
+ */
+public class DelegatedExecutorService extends AbstractExecutorService {
+	private final ExecutorService e;
+
+	DelegatedExecutorService(ExecutorService executor) {
+		e = executor;
+	}
+
+	@SuppressWarnings("NullableProblems")
+	public void execute(Runnable command) {
+		e.execute(command);
+	}
+
+	public void shutdown() {
+		e.shutdown();
+	}
+
+	@SuppressWarnings("NullableProblems")
+	public List<Runnable> shutdownNow() {
+		return e.shutdownNow();
+	}
+
+	public boolean isShutdown() {
+		return e.isShutdown();
+	}
+
+	public boolean isTerminated() {
+		return e.isTerminated();
+	}
+
+	@SuppressWarnings("NullableProblems")
+	public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
+		return e.awaitTermination(timeout, unit);
+	}
+
+	@SuppressWarnings("NullableProblems")
+	public Future<?> submit(Runnable task) {
+		return e.submit(task);
+	}
+
+	@SuppressWarnings("NullableProblems")
+	public <T> Future<T> submit(Callable<T> task) {
+		return e.submit(task);
+	}
+
+	@SuppressWarnings("NullableProblems")
+	public <T> Future<T> submit(Runnable task, T result) {
+		return e.submit(task, result);
+	}
+
+	@SuppressWarnings("NullableProblems")
+	public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
+		return e.invokeAll(tasks);
+	}
+
+	@SuppressWarnings("NullableProblems")
+	public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
+			throws InterruptedException {
+		return e.invokeAll(tasks, timeout, unit);
+	}
+
+	@SuppressWarnings("NullableProblems")
+	public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
+			throws InterruptedException, ExecutionException {
+		return e.invokeAny(tasks);
+	}
+
+	public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
+			throws InterruptedException, ExecutionException, TimeoutException {
+		return e.invokeAny(tasks, timeout, unit);
+	}
+}

+ 68 - 26
hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java

@@ -1,6 +1,8 @@
 package cn.hutool.core.thread;
 
+import java.util.concurrent.ArrayBlockingQueue;
 import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.RejectedExecutionHandler;
@@ -14,31 +16,48 @@ import cn.hutool.core.util.ObjectUtil;
 
 /**
  * {@link ThreadPoolExecutor} 建造者
- * 
+ *
  * @author looly
  * @since 4.1.9
  */
 public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 	private static final long serialVersionUID = 1L;
 
-	/** 初始池大小 */
+	/** 默认的等待队列容量 */
+	public static final int DEFAULT_QUEUE_CAPACITY = 1024;
+
+	/**
+	 * 初始池大小
+	 */
 	private int corePoolSize;
-	/** 最大池大小(允许同时执行的最大线程数) */
+	/**
+	 * 最大池大小(允许同时执行的最大线程数)
+	 */
 	private int maxPoolSize = Integer.MAX_VALUE;
-	/** 线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长 */
+	/**
+	 * 线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长
+	 */
 	private long keepAliveTime = TimeUnit.SECONDS.toNanos(60);
-	/** 队列,用于存在未执行的线程 */
+	/**
+	 * 队列,用于存在未执行的线程
+	 */
 	private BlockingQueue<Runnable> workQueue;
-	/** 线程工厂,用于自定义线程创建 */
+	/**
+	 * 线程工厂,用于自定义线程创建
+	 */
 	private ThreadFactory threadFactory;
-	/** 当线程阻塞(block)时的异常处理器,所谓线程阻塞即线程池和等待队列已满,无法处理线程时采取的策略 */
+	/**
+	 * 当线程阻塞(block)时的异常处理器,所谓线程阻塞即线程池和等待队列已满,无法处理线程时采取的策略
+	 */
 	private RejectedExecutionHandler handler;
-	/** 线程执行超时后是否回收线程 */
+	/**
+	 * 线程执行超时后是否回收线程
+	 */
 	private Boolean allowCoreThreadTimeOut;
 
 	/**
 	 * 设置初始池大小,默认0
-	 * 
+	 *
 	 * @param corePoolSize 初始池大小
 	 * @return this
 	 */
@@ -49,7 +68,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 
 	/**
 	 * 设置最大池大小(允许同时执行的最大线程数)
-	 * 
+	 *
 	 * @param maxPoolSize 最大池大小(允许同时执行的最大线程数)
 	 * @return this
 	 */
@@ -60,9 +79,9 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 
 	/**
 	 * 设置线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长
-	 * 
+	 *
 	 * @param keepAliveTime 线程存活时间
-	 * @param unit 单位
+	 * @param unit          单位
 	 * @return this
 	 */
 	public ExecutorBuilder setKeepAliveTime(long keepAliveTime, TimeUnit unit) {
@@ -71,7 +90,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 
 	/**
 	 * 设置线程存活时间,即当池中线程多于初始大小时,多出的线程保留的时长,单位纳秒
-	 * 
+	 *
 	 * @param keepAliveTime 线程存活时间,单位纳秒
 	 * @return this
 	 */
@@ -83,13 +102,14 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 	/**
 	 * 设置队列,用于存在未执行的线程<br>
 	 * 可选队列有:
-	 * 
+	 *
 	 * <pre>
-	 * 1. SynchronousQueue    它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
-	 * 2. LinkedBlockingQueue 无界队列,当运行线程大于corePoolSize时始终放入此队列,此时maximumPoolSize无效
-	 * 3. ArrayBlockingQueue  有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
+	 * 1. {@link SynchronousQueue}    它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
+	 * 2. {@link LinkedBlockingQueue} 默认无界队列,当运行线程大于corePoolSize时始终放入此队列,此时maximumPoolSize无效。
+	 *                        当构造LinkedBlockingQueue对象时传入参数,变为有界队列,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
+	 * 3. {@link ArrayBlockingQueue}  有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
 	 * </pre>
-	 * 
+	 *
 	 * @param workQueue 队列
 	 * @return this
 	 */
@@ -99,9 +119,21 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 	}
 
 	/**
+	 * 使用{@link ArrayBlockingQueue} 做为等待队列<br>
+	 * 有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
+	 *
+	 * @param capacity 队列容量
+	 * @return this
+	 * @since 5.1.4
+	 */
+	public ExecutorBuilder useArrayBlockingQueue(int capacity) {
+		return setWorkQueue(new ArrayBlockingQueue<>(capacity));
+	}
+
+	/**
 	 * 使用{@link SynchronousQueue} 做为等待队列(非公平策略)<br>
 	 * 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
-	 * 
+	 *
 	 * @return this
 	 * @since 4.1.11
 	 */
@@ -112,7 +144,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 	/**
 	 * 使用{@link SynchronousQueue} 做为等待队列<br>
 	 * 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略
-	 * 
+	 *
 	 * @param fair 是否使用公平访问策略
 	 * @return this
 	 * @since 4.5.0
@@ -123,7 +155,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 
 	/**
 	 * 设置线程工厂,用于自定义线程创建
-	 * 
+	 *
 	 * @param threadFactory 线程工厂
 	 * @return this
 	 * @see ThreadFactoryBuilder
@@ -137,7 +169,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 	 * 设置当线程阻塞(block)时的异常处理器,所谓线程阻塞即线程池和等待队列已满,无法处理线程时采取的策略
 	 * <p>
 	 * 此处可以使用JDK预定义的几种策略,见{@link RejectPolicy}枚举
-	 * 
+	 *
 	 * @param handler {@link RejectedExecutionHandler}
 	 * @return this
 	 * @see RejectPolicy
@@ -149,7 +181,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 
 	/**
 	 * 设置线程执行超时后是否回收线程
-	 * 
+	 *
 	 * @param allowCoreThreadTimeOut 线程执行超时后是否回收线程
 	 * @return this
 	 */
@@ -160,7 +192,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 
 	/**
 	 * 创建ExecutorBuilder,开始构建
-	 * 
+	 *
 	 * @return {@link ExecutorBuilder}
 	 */
 	public static ExecutorBuilder create() {
@@ -176,8 +208,18 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 	}
 
 	/**
+	 * 创建有回收关闭功能的ExecutorService
+	 *
+	 * @return 创建有回收关闭功能的ExecutorService
+	 * @since 5.1.4
+	 */
+	public ExecutorService buildFinalizable() {
+		return new FinalizableDelegatedExecutorService(build());
+	}
+
+	/**
 	 * 构建ThreadPoolExecutor
-	 * 
+	 *
 	 * @param builder {@link ExecutorBuilder}
 	 * @return {@link ThreadPoolExecutor}
 	 */
@@ -190,7 +232,7 @@ public class ExecutorBuilder implements Builder<ThreadPoolExecutor> {
 			workQueue = builder.workQueue;
 		} else {
 			// corePoolSize为0则要使用SynchronousQueue避免无限阻塞
-			workQueue = (corePoolSize <= 0) ? new SynchronousQueue<>() : new LinkedBlockingQueue<>();
+			workQueue = (corePoolSize <= 0) ? new SynchronousQueue<>() : new LinkedBlockingQueue<>(DEFAULT_QUEUE_CAPACITY);
 		}
 		final ThreadFactory threadFactory = (null != builder.threadFactory) ? builder.threadFactory : Executors.defaultThreadFactory();
 		RejectedExecutionHandler handler = ObjectUtil.defaultIfNull(builder.handler, new ThreadPoolExecutor.AbortPolicy());

+ 19 - 0
hutool-core/src/main/java/cn/hutool/core/thread/FinalizableDelegatedExecutorService.java

@@ -0,0 +1,19 @@
+package cn.hutool.core.thread;
+
+import java.util.concurrent.ExecutorService;
+
+/**
+ * 保证ExecutorService在对象回收时正常结束
+ *
+ * @author loolly
+ */
+public class FinalizableDelegatedExecutorService extends DelegatedExecutorService {
+	FinalizableDelegatedExecutorService(ExecutorService executor) {
+		super(executor);
+	}
+
+	@Override
+	protected void finalize() {
+		super.shutdown();
+	}
+}

+ 5 - 2
hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java

@@ -6,7 +6,6 @@ import java.util.concurrent.CompletionService;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorCompletionService;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.ThreadFactory;
 import java.util.concurrent.ThreadPoolExecutor;
@@ -48,7 +47,11 @@ public class ThreadUtil {
 	 * @return ExecutorService
 	 */
 	public static ExecutorService newSingleExecutor() {
-		return Executors.newSingleThreadExecutor();
+		return ExecutorBuilder.create()//
+				.setCorePoolSize(1)//
+				.setMaxPoolSize(1)//
+				.setKeepAliveTime(0)//
+				.buildFinalizable();
 	}
 
 	/**

+ 35 - 31
hutool-extra/src/main/java/cn/hutool/extra/servlet/ServletUtil.java

@@ -1,25 +1,5 @@
 package cn.hutool.extra.servlet;
 
-import java.io.BufferedInputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.PrintWriter;
-import java.io.Writer;
-import java.lang.reflect.Type;
-import java.nio.charset.Charset;
-import java.util.Collections;
-import java.util.Date;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.servlet.ServletOutputStream;
-import javax.servlet.ServletRequest;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.bean.copier.CopyOptions;
 import cn.hutool.core.bean.copier.ValueProvider;
@@ -37,6 +17,25 @@ import cn.hutool.core.util.URLUtil;
 import cn.hutool.extra.servlet.multipart.MultipartFormData;
 import cn.hutool.extra.servlet.multipart.UploadSetting;
 
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Servlet相关工具类封装
  * 
@@ -128,16 +127,21 @@ public class ServletUtil {
 		return BeanUtil.fillBean(bean, new ValueProvider<String>() {
 			@Override
 			public Object value(String key, Type valueType) {
-				String value = request.getParameter(key);
-				if (StrUtil.isEmpty(value)) {
-					// 使用类名前缀尝试查找值
-					value = request.getParameter(beanName + StrUtil.DOT + key);
-					if (StrUtil.isEmpty(value)) {
-						// 此处取得的值为空时跳过,包括null和""
-						value = null;
+				String[] values = request.getParameterValues(key);
+				if(ArrayUtil.isEmpty(values)){
+					values = request.getParameterValues(beanName + StrUtil.DOT + key);
+					if(ArrayUtil.isEmpty(values)){
+						return null;
 					}
 				}
-				return value;
+
+				if(1 == values.length){
+					// 单值表单直接返回这个值
+					return values[0];
+				}else{
+					// 多值表单返回数组
+					return values;
+				}
 			}
 
 			@Override
@@ -346,6 +350,7 @@ public class ServletUtil {
 	public static boolean isIE(HttpServletRequest request) {
 		String userAgent = getHeaderIgnoreCase(request, "User-Agent");
 		if (StrUtil.isNotBlank(userAgent)) {
+			//noinspection ConstantConditions
 			userAgent = userAgent.toUpperCase();
 			return userAgent.contains("MSIE") || userAgent.contains("TRIDENT");
 		}
@@ -400,8 +405,7 @@ public class ServletUtil {
 	 * @return Cookie对象
 	 */
 	public static Cookie getCookie(HttpServletRequest httpServletRequest, String name) {
-		final Map<String, Cookie> cookieMap = readCookieMap(httpServletRequest);
-		return cookieMap == null ? null : cookieMap.get(name);
+		return readCookieMap(httpServletRequest).get(name);
 	}
 
 	/**
@@ -604,7 +608,7 @@ public class ServletUtil {
 		} else if (Date.class.isAssignableFrom(value.getClass())) {
 			response.setDateHeader(name, ((Date) value).getTime());
 		} else if (value instanceof Integer || "int".equals(value.getClass().getSimpleName().toLowerCase())) {
-			response.setIntHeader(name, (Integer) value);
+			response.setIntHeader(name, (int) value);
 		} else {
 			response.setHeader(name, value.toString());
 		}

+ 1 - 1
hutool-extra/src/test/java/cn/hutool/extra/ftp/FtpTest.java

@@ -10,7 +10,7 @@ import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.lang.Console;
 
 public class FtpTest {
-	
+
 	@Test
 	@Ignore
 	public void cdTest() {

+ 0 - 5
hutool-log/src/main/java/cn/hutool/log/dialect/log4j2/Log4j2Log.java

@@ -55,11 +55,6 @@ public class Log4j2Log extends AbstractLog {
 	}
 
 	@Override
-	public void debug(String format, Object... arguments) {
-		debug(null, format, arguments);
-	}
-
-	@Override
 	public void debug(String fqcn, Throwable t, String format, Object... arguments) {
 		logIfEnabled(fqcn, Level.DEBUG, t, format, arguments);
 	}

+ 3 - 1
hutool-log/src/test/java/cn/hutool/log/test/CustomLogTest.java

@@ -57,7 +57,9 @@ public class CustomLogTest {
 		LogFactory factory = new Log4j2LogFactory();
 		LogFactory.setCurrentLogFactory(factory);
 		Log log = LogFactory.get();
-		
+
+		log.debug(null);
+		log.debug("This is custom '{}' log\n{}", factory.getName(), LINE);
 		log.info(null);
 		log.info("This is custom '{}' log\n{}", factory.getName(), LINE);
 	}

+ 31 - 17
hutool-poi/src/main/java/cn/hutool/poi/excel/sax/Excel03SaxReader.java

@@ -1,12 +1,11 @@
 package cn.hutool.poi.excel.sax;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
-
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.lang.Console;
 import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.poi.excel.sax.handler.RowHandler;
+import cn.hutool.poi.exceptions.POIException;
 import org.apache.poi.hssf.eventusermodel.EventWorkbookBuilder.SheetRecordCollectingListener;
 import org.apache.poi.hssf.eventusermodel.FormatTrackingHSSFListener;
 import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
@@ -30,10 +29,11 @@ import org.apache.poi.hssf.record.StringRecord;
 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
 import org.apache.poi.poifs.filesystem.POIFSFileSystem;
 
-import cn.hutool.core.io.IoUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.poi.excel.sax.handler.RowHandler;
-import cn.hutool.poi.exceptions.POIException;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Excel2003格式的事件-用户模型方式读取器,在Hutool中,统一将此归类为Sax读取<br>
@@ -194,7 +194,7 @@ public class Excel03SaxReader extends AbstractExcelSaxReader<Excel03SaxReader> i
 			if (record instanceof MissingCellDummyRecord) {
 				// 空值的操作
 				MissingCellDummyRecord mc = (MissingCellDummyRecord) record;
-				rowCellList.add(mc.getColumn(), StrUtil.EMPTY);
+				addToRowCellList(mc.getColumn(), StrUtil.EMPTY);
 			} else if (record instanceof LastCellOfRowDummyRecord) {
 				// 行结束
 				processLastCell((LastCellOfRowDummyRecord) record);
@@ -209,6 +209,20 @@ public class Excel03SaxReader extends AbstractExcelSaxReader<Excel03SaxReader> i
 	// ---------------------------------------------------------------------------------------------- Private method start
 
 	/**
+	 * 将单元格数据加入到行列表中
+	 * @param index 加入位置
+	 * @param value 值
+	 */
+	private void addToRowCellList(int index, Object value){
+		while(index > this.rowCellList.size()){
+			// 对于中间无数据的单元格补齐空白
+			this.rowCellList.add(StrUtil.EMPTY);
+		}
+
+		this.rowCellList.add(index, value);
+	}
+
+	/**
 	 * 处理单元格值
 	 *
 	 * @param record 单元格
@@ -219,12 +233,12 @@ public class Excel03SaxReader extends AbstractExcelSaxReader<Excel03SaxReader> i
 		switch (record.getSid()) {
 			case BlankRecord.sid:
 				// 空白记录
-				rowCellList.add(((BlankRecord) record).getColumn(), StrUtil.EMPTY);
+				addToRowCellList(((BlankRecord) record).getColumn(), StrUtil.EMPTY);
 				break;
 			case BoolErrRecord.sid:
 				// 布尔类型
 				final BoolErrRecord berec = (BoolErrRecord) record;
-				rowCellList.add(berec.getColumn(), berec.getBooleanValue());
+				addToRowCellList(berec.getColumn(), berec.getBooleanValue());
 				break;
 			case FormulaRecord.sid:
 				// 公式类型
@@ -240,7 +254,7 @@ public class Excel03SaxReader extends AbstractExcelSaxReader<Excel03SaxReader> i
 				} else {
 					value = StrUtil.wrap(HSSFFormulaParser.toFormulaString(stubWorkbook, formulaRec.getParsedExpression()), "\"");
 				}
-				rowCellList.add(formulaRec.getColumn(), value);
+				addToRowCellList(formulaRec.getColumn(), value);
 				break;
 			case StringRecord.sid:
 				// 单元格中公式的字符串
@@ -253,7 +267,7 @@ public class Excel03SaxReader extends AbstractExcelSaxReader<Excel03SaxReader> i
 			case LabelRecord.sid:
 				final LabelRecord lrec = (LabelRecord) record;
 				value = lrec.getValue();
-				this.rowCellList.add(lrec.getColumn(), value);
+				addToRowCellList(lrec.getColumn(), value);
 				break;
 			case LabelSSTRecord.sid:
 				// 字符串类型
@@ -261,7 +275,7 @@ public class Excel03SaxReader extends AbstractExcelSaxReader<Excel03SaxReader> i
 				if (null != sstRecord) {
 					value = sstRecord.getString(lsrec.getSSTIndex()).toString();
 				}
-				rowCellList.add(lsrec.getColumn(), ObjectUtil.defaultIfNull(value, StrUtil.EMPTY));
+				addToRowCellList(lsrec.getColumn(), ObjectUtil.defaultIfNull(value, StrUtil.EMPTY));
 				break;
 			case NumberRecord.sid: // 数字类型
 				final NumberRecord numrec = (NumberRecord) record;
@@ -283,7 +297,7 @@ public class Excel03SaxReader extends AbstractExcelSaxReader<Excel03SaxReader> i
 					}
 				}
 				// 向容器加入列值
-				rowCellList.add(numrec.getColumn(), value);
+				addToRowCellList(numrec.getColumn(), value);
 				break;
 			default:
 				break;

+ 20 - 15
hutool-poi/src/test/java/cn/hutool/poi/excel/test/ExcelSaxReadTest.java

@@ -20,6 +20,20 @@ import org.junit.Test;
 public class ExcelSaxReadTest {
 
 	@Test
+	public void excel07Test() {
+		// 工具化快速读取
+		ExcelUtil.read07BySax("aaa.xlsx", 0, createRowHandler());
+	}
+
+	@Test
+	public void excel03Test() {
+		Excel03SaxReader reader = new Excel03SaxReader(createRowHandler());
+		reader.read("aaa.xls", 1);
+		// Console.log("Sheet index: [{}], Sheet name: [{}]", reader.getSheetIndex(), reader.getSheetName());
+		ExcelUtil.read03BySax("aaa.xls", 1, createRowHandler());
+	}
+
+	@Test
 	@Ignore
 	public void readBlankLineTest() {
 		ExcelUtil.readBySax("e:/ExcelBlankLine.xlsx", 0, (sheetIndex, rowIndex, rowList) -> {
@@ -48,25 +62,11 @@ public class ExcelSaxReadTest {
 	}
 
 	@Test
-	public void excel07Test() {
-		// 工具化快速读取
-		ExcelUtil.read07BySax("aaa.xlsx", 0, createRowHandler());
-	}
-
-	@Test
-	public void excel03Test() {
-		Excel03SaxReader reader = new Excel03SaxReader(createRowHandler());
-		reader.read("aaa.xls", 1);
-		// Console.log("Sheet index: [{}], Sheet name: [{}]", reader.getSheetIndex(), reader.getSheetName());
-		ExcelUtil.read03BySax("aaa.xls", 1, createRowHandler());
-	}
-
-	@Test
 	@Ignore
 	public void readBySaxTest4() {
 		ExcelUtil.readBySax("e:/excel/single_line.xlsx", 2, createRowHandler());
 	}
-	
+
 	@Test
 	@Ignore
 	public void readBySaxTest5() {
@@ -79,6 +79,11 @@ public class ExcelSaxReadTest {
 		ExcelUtil.readBySax("f:\\test\\sax_test.xlsx", 0, createRowHandler());
 	}
 
+	@Test
+	public void readBySaxTest7() {
+		ExcelUtil.readBySax("d:/test/行政许可信息.xls", 0, (sheetIndex, rowIndex, rowList) -> Console.log(rowList));
+	}
+
 	private RowHandler createRowHandler() {
 		return (sheetIndex, rowIndex, rowlist) -> {
 //				Console.log("[{}] [{}] {}", sheetIndex, rowIndex, rowlist);