diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java index 14ed82bf2..9cd91a0e3 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/analysis/ExcelAnalyserImpl.java @@ -49,11 +49,8 @@ import org.apache.fesod.sheet.read.metadata.holder.xls.XlsReadWorkbookHolder; import org.apache.fesod.sheet.read.metadata.holder.xlsx.XlsxReadWorkbookHolder; import org.apache.fesod.sheet.support.ExcelTypeEnum; -import org.apache.fesod.sheet.util.ClassUtils; -import org.apache.fesod.sheet.util.DateUtils; import org.apache.fesod.sheet.util.FileUtils; -import org.apache.fesod.sheet.util.NumberDataFormatterUtils; -import org.apache.fesod.sheet.util.NumberUtils; +import org.apache.fesod.sheet.util.ThreadLocalCleaner; import org.apache.poi.hssf.OldExcelFormatException; import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.poifs.crypt.Decryptor; @@ -281,23 +278,13 @@ public void finish() { clearEncrypt03(); - removeThreadLocalCache(); + ThreadLocalCleaner.clearAllThreadLocals(); if (throwable != null) { throw new ExcelAnalysisException("Can not close IO.", throwable); } } - /** - * Removes thread-local caches used during the analysis process to free up memory. - */ - private void removeThreadLocalCache() { - NumberDataFormatterUtils.removeThreadLocalCache(); - NumberUtils.removeThreadLocalCache(); - DateUtils.removeThreadLocalCache(); - ClassUtils.removeThreadLocalCache(); - } - /** * Clears the encryption key for XLS files to ensure secure handling of sensitive data. */ diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 17fcc7aad..ab442ad13 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -46,10 +46,8 @@ import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; import org.apache.fesod.sheet.support.ExcelTypeEnum; import org.apache.fesod.sheet.util.ClassUtils; -import org.apache.fesod.sheet.util.DateUtils; import org.apache.fesod.sheet.util.FileUtils; -import org.apache.fesod.sheet.util.NumberDataFormatterUtils; -import org.apache.fesod.sheet.util.NumberUtils; +import org.apache.fesod.sheet.util.ThreadLocalCleaner; import org.apache.fesod.sheet.util.WorkBookUtil; import org.apache.fesod.sheet.util.WriteHandlerUtils; import org.apache.fesod.sheet.write.handler.context.CellWriteHandlerContext; @@ -584,7 +582,7 @@ public void finish(boolean onException) { throwable = recordFailure(throwable, t); } clearEncrypt03(); - removeThreadLocalCache(); + ThreadLocalCleaner.clearAllThreadLocals(); if (throwable != null) { throw new ExcelGenerateException("Can not close IO.", throwable); } @@ -606,16 +604,6 @@ private static Throwable recordFailure(Throwable recorded, Throwable t) { return recorded; } - /** - * Removes thread-local caches used during the write process. - */ - private void removeThreadLocalCache() { - NumberDataFormatterUtils.removeThreadLocalCache(); - NumberUtils.removeThreadLocalCache(); - DateUtils.removeThreadLocalCache(); - ClassUtils.removeThreadLocalCache(); - } - /** * Retrieves the current sheet being written. * diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java index 3116e9bf9..64cf8f153 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ClassUtils.java @@ -102,6 +102,14 @@ public class ClassUtils { private static final ThreadLocal> CONTENT_THREAD_LOCAL = new ThreadLocal<>(); + static { + ThreadLocalCleaner.register(() -> { + FIELD_THREAD_LOCAL.remove(); + CLASS_CONTENT_THREAD_LOCAL.remove(); + CONTENT_THREAD_LOCAL.remove(); + }); + } + /** * Calculate the configuration information for the class * diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java index 062d890ac..f1c1d9acc 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/DateUtils.java @@ -72,6 +72,13 @@ public class DateUtils { private static final ThreadLocal>> DATE_TIME_FORMATTER_THREAD_LOCAL = new ThreadLocal<>(); + static { + ThreadLocalCleaner.register(() -> { + DATE_FORMAT_THREAD_LOCAL.remove(); + DATE_TIME_FORMATTER_THREAD_LOCAL.remove(); + }); + } + /** * The epoch date (1970-01-01) used as the date component when converting * {@code java.sql.Time} to {@code LocalDateTime}. diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExcelDateFormatDetector.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExcelDateFormatDetector.java index b144e2d43..5dd7feb6a 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExcelDateFormatDetector.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ExcelDateFormatDetector.java @@ -54,6 +54,10 @@ class ExcelDateFormatDetector { // for format which start with "年" or "月" or "日" or "时" or "分" or "秒" could be a Chinese date private static final Pattern date_ptrn6 = Pattern.compile("(年|月|日|时|分|秒)+"); + static { + ThreadLocalCleaner.register(DATE_THREAD_LOCAL::remove); + } + private ExcelDateFormatDetector() {} /** diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberDataFormatterUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberDataFormatterUtils.java index 67c293a37..7fa78890b 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberDataFormatterUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberDataFormatterUtils.java @@ -63,6 +63,10 @@ private boolean matches(boolean use1904windowing, Locale locale, boolean useScie } } + static { + ThreadLocalCleaner.register(DATA_FORMATTER_THREAD_LOCAL::remove); + } + /** * Format number data. * diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberUtils.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberUtils.java index 49d918f9d..3da024160 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberUtils.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/NumberUtils.java @@ -56,6 +56,10 @@ public class NumberUtils { private static final ThreadLocal>> DECIMAL_FORMAT_THREAD_LOCAL = new ThreadLocal<>(); + static { + ThreadLocalCleaner.register(DECIMAL_FORMAT_THREAD_LOCAL::remove); + } + private NumberUtils() {} /** diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ThreadLocalCleaner.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ThreadLocalCleaner.java new file mode 100644 index 000000000..399747516 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/ThreadLocalCleaner.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.util; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.Validate; + +/** + * Utility class for managing and executing unified {@link ThreadLocal} cleanups. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class ThreadLocalCleaner { + + private static final List cleanTasks = new CopyOnWriteArrayList<>(); + + /** + * Registers a cleanup task. + *

NOTE: for internal use only.

+ * + * @param cleaner the cleanup task to register + */ + public static void register(Runnable cleaner) { + Validate.notNull(cleaner, "cleaner cannot be null"); + cleanTasks.add(cleaner); + } + + public static void clearAllThreadLocals() { + cleanTasks.forEach(Runnable::run); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ThreadLocalCleanerTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ThreadLocalCleanerTest.java new file mode 100644 index 000000000..9575cb84e --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ThreadLocalCleanerTest.java @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.fesod.sheet.util; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link ThreadLocalCleaner}. + */ +class ThreadLocalCleanerTest { + + @Test + void testRegisterNullThrowsException() { + Assertions.assertThrows(NullPointerException.class, () -> ThreadLocalCleaner.register(null)); + } + + @Test + void testConcurrentModificationExceptionRegression() throws Exception { + CountDownLatch iterationStartedLatch = new CountDownLatch(1); + CountDownLatch registrationDoneLatch = new CountDownLatch(1); + + AtomicBoolean task1Executed = new AtomicBoolean(false); + AtomicBoolean task2Executed = new AtomicBoolean(false); + + ThreadLocalCleaner.register(() -> { + task1Executed.set(true); + iterationStartedLatch.countDown(); + try { + if (!registrationDoneLatch.await(5, TimeUnit.SECONDS)) { + throw new IllegalStateException("Timeout waiting for concurrent registration"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + + CompletableFuture clearFuture = CompletableFuture.runAsync(ThreadLocalCleaner::clearAllThreadLocals); + + Assertions.assertTrue(iterationStartedLatch.await(5, TimeUnit.SECONDS), "Iteration did not start in time"); + + ThreadLocalCleaner.register(() -> task2Executed.set(true)); + registrationDoneLatch.countDown(); + + Assertions.assertDoesNotThrow(() -> clearFuture.get(5, TimeUnit.SECONDS)); + Assertions.assertTrue(task1Executed.get(), "Task 1 should have executed"); + + ThreadLocalCleaner.clearAllThreadLocals(); + Assertions.assertTrue(task2Executed.get(), "Task 2 should execute in subsequent clear"); + } +}