From e6131b425cf69c688a71b94e06647ada24cdddeb Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sat, 12 Sep 2026 22:53:29 +0300 Subject: [PATCH 01/20] refactor: split ClassUtils into cache resolvers --- .../apache/fesod/sheet/util/ClassUtils.java | 421 +----------------- .../sheet/util/MetadataCacheStrategy.java | 114 +++++ .../util/SheetContentPropertyResolver.java | 214 +++++++++ .../sheet/util/SheetHeadFieldResolver.java | 277 ++++++++++++ .../fesod/sheet/readwrite/CacheDataTest.java | 6 +- 5 files changed, 613 insertions(+), 419 deletions(-) create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java 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..078e17595 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 @@ -25,46 +25,21 @@ package org.apache.fesod.sheet.util; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.fesod.common.util.ListUtils; -import org.apache.fesod.common.util.MapUtils; -import org.apache.fesod.shaded.cglib.beans.BeanMap; -import org.apache.fesod.sheet.annotation.ExcelIgnore; -import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated; -import org.apache.fesod.sheet.annotation.ExcelProperty; -import org.apache.fesod.sheet.annotation.format.DateTimeFormat; -import org.apache.fesod.sheet.annotation.format.NumberFormat; -import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; -import org.apache.fesod.sheet.annotation.write.style.ContentStyle; -import org.apache.fesod.sheet.converters.AutoConverter; -import org.apache.fesod.sheet.converters.Converter; -import org.apache.fesod.sheet.exception.ExcelCommonException; import org.apache.fesod.sheet.metadata.ConfigurationHolder; import org.apache.fesod.sheet.metadata.FieldCache; -import org.apache.fesod.sheet.metadata.FieldWrapper; -import org.apache.fesod.sheet.metadata.property.DateTimeFormatProperty; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; -import org.apache.fesod.sheet.metadata.property.FontProperty; -import org.apache.fesod.sheet.metadata.property.NumberFormatProperty; -import org.apache.fesod.sheet.metadata.property.StyleProperty; import org.apache.fesod.sheet.write.metadata.holder.WriteHolder; public class ClassUtils { @@ -73,10 +48,6 @@ public class ClassUtils { * memory cache */ public static final Map FIELD_CACHE = new ConcurrentHashMap<>(); - /** - * thread local cache - */ - private static final ThreadLocal> FIELD_THREAD_LOCAL = new ThreadLocal<>(); /** * The cache configuration information for each of the class @@ -84,24 +55,12 @@ public class ClassUtils { public static final ConcurrentHashMap, Map> CLASS_CONTENT_CACHE = new ConcurrentHashMap<>(); - /** - * The cache configuration information for each of the class - */ - private static final ThreadLocal, Map>> CLASS_CONTENT_THREAD_LOCAL = - new ThreadLocal<>(); - /** * The cache configuration information for each of the class */ public static final ConcurrentHashMap CONTENT_CACHE = new ConcurrentHashMap<>(); - /** - * The cache configuration information for each of the class - */ - private static final ThreadLocal> CONTENT_THREAD_LOCAL = - new ThreadLocal<>(); - /** * Calculate the configuration information for the class * @@ -112,161 +71,12 @@ public class ClassUtils { */ public static ExcelContentProperty declaredExcelContentProperty( Map dataMap, Class headClazz, String fieldName, ConfigurationHolder configurationHolder) { - Class clazz = null; - if (dataMap instanceof BeanMap) { - Object bean = ((BeanMap) dataMap).getBean(); - if (bean != null) { - clazz = bean.getClass(); - } - } - return getExcelContentProperty(clazz, headClazz, fieldName, configurationHolder); - } - - private static ExcelContentProperty getExcelContentProperty( - Class clazz, Class headClass, String fieldName, ConfigurationHolder configurationHolder) { - switch (configurationHolder.globalConfiguration().getFiledCacheLocation()) { - case THREAD_LOCAL: - Map contentCacheMap = CONTENT_THREAD_LOCAL.get(); - if (contentCacheMap == null) { - contentCacheMap = MapUtils.newHashMap(); - CONTENT_THREAD_LOCAL.set(contentCacheMap); - } - return contentCacheMap.computeIfAbsent(buildKey(clazz, headClass, fieldName), key -> { - return doGetExcelContentProperty(clazz, headClass, fieldName, configurationHolder); - }); - case MEMORY: - return CONTENT_CACHE.computeIfAbsent(buildKey(clazz, headClass, fieldName), key -> { - return doGetExcelContentProperty(clazz, headClass, fieldName, configurationHolder); - }); - case NONE: - return doGetExcelContentProperty(clazz, headClass, fieldName, configurationHolder); - default: - throw new UnsupportedOperationException("unsupported enum"); - } - } - - private static ExcelContentProperty doGetExcelContentProperty( - Class clazz, Class headClass, String fieldName, ConfigurationHolder configurationHolder) { - ExcelContentProperty excelContentProperty = Optional.ofNullable( - declaredFieldContentMap(clazz, configurationHolder)) - .map(map -> map.get(fieldName)) - .orElse(null); - ExcelContentProperty headExcelContentProperty = Optional.ofNullable( - declaredFieldContentMap(headClass, configurationHolder)) - .map(map -> map.get(fieldName)) - .orElse(null); - ExcelContentProperty combineExcelContentProperty = new ExcelContentProperty(); - - combineExcelContentProperty(combineExcelContentProperty, headExcelContentProperty); - if (clazz != headClass) { - combineExcelContentProperty(combineExcelContentProperty, excelContentProperty); - } - return combineExcelContentProperty; + return SheetContentPropertyResolver.resolve(dataMap, headClazz, fieldName, configurationHolder); } public static void combineExcelContentProperty( ExcelContentProperty combineExcelContentProperty, ExcelContentProperty excelContentProperty) { - if (excelContentProperty == null) { - return; - } - if (excelContentProperty.getField() != null) { - combineExcelContentProperty.setField(excelContentProperty.getField()); - } - if (excelContentProperty.getConverter() != null) { - combineExcelContentProperty.setConverter(excelContentProperty.getConverter()); - } - if (excelContentProperty.getDateTimeFormatProperty() != null) { - combineExcelContentProperty.setDateTimeFormatProperty(excelContentProperty.getDateTimeFormatProperty()); - } - if (excelContentProperty.getNumberFormatProperty() != null) { - combineExcelContentProperty.setNumberFormatProperty(excelContentProperty.getNumberFormatProperty()); - } - if (excelContentProperty.getContentStyleProperty() != null) { - combineExcelContentProperty.setContentStyleProperty(excelContentProperty.getContentStyleProperty()); - } - if (excelContentProperty.getContentFontProperty() != null) { - combineExcelContentProperty.setContentFontProperty(excelContentProperty.getContentFontProperty()); - } - } - - private static ContentPropertyKey buildKey(Class clazz, Class headClass, String fieldName) { - return new ContentPropertyKey(clazz, headClass, fieldName); - } - - private static Map declaredFieldContentMap( - Class clazz, ConfigurationHolder configurationHolder) { - if (clazz == null) { - return null; - } - switch (configurationHolder.globalConfiguration().getFiledCacheLocation()) { - case THREAD_LOCAL: - Map, Map> classContentCacheMap = - CLASS_CONTENT_THREAD_LOCAL.get(); - if (classContentCacheMap == null) { - classContentCacheMap = MapUtils.newHashMap(); - CLASS_CONTENT_THREAD_LOCAL.set(classContentCacheMap); - } - return classContentCacheMap.computeIfAbsent(clazz, key -> { - return doDeclaredFieldContentMap(clazz); - }); - case MEMORY: - return CLASS_CONTENT_CACHE.computeIfAbsent(clazz, key -> { - return doDeclaredFieldContentMap(clazz); - }); - case NONE: - return doDeclaredFieldContentMap(clazz); - default: - throw new UnsupportedOperationException("unsupported enum"); - } - } - - private static Map doDeclaredFieldContentMap(Class clazz) { - if (clazz == null) { - return null; - } - List tempFieldList = FieldUtils.resolveAllFields(clazz); - - ContentStyle parentContentStyle = clazz.getAnnotation(ContentStyle.class); - ContentFontStyle parentContentFontStyle = clazz.getAnnotation(ContentFontStyle.class); - Map fieldContentMap = MapUtils.newHashMapWithExpectedSize(tempFieldList.size()); - for (Field field : tempFieldList) { - ExcelContentProperty excelContentProperty = new ExcelContentProperty(); - excelContentProperty.setField(field); - - ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); - if (excelProperty != null) { - Class> convertClazz = excelProperty.converter(); - if (convertClazz != AutoConverter.class) { - try { - Converter converter = - convertClazz.getDeclaredConstructor().newInstance(); - excelContentProperty.setConverter(converter); - } catch (Exception e) { - throw new ExcelCommonException("Can not instance custom converter:" + convertClazz.getName()); - } - } - } - - ContentStyle contentStyle = field.getAnnotation(ContentStyle.class); - if (contentStyle == null) { - contentStyle = parentContentStyle; - } - excelContentProperty.setContentStyleProperty(StyleProperty.build(contentStyle)); - - ContentFontStyle contentFontStyle = field.getAnnotation(ContentFontStyle.class); - if (contentFontStyle == null) { - contentFontStyle = parentContentFontStyle; - } - excelContentProperty.setContentFontProperty(FontProperty.build(contentFontStyle)); - - excelContentProperty.setDateTimeFormatProperty( - DateTimeFormatProperty.build(field.getAnnotation(DateTimeFormat.class))); - excelContentProperty.setNumberFormatProperty( - NumberFormatProperty.build(field.getAnnotation(NumberFormat.class))); - - fieldContentMap.put(field.getName(), excelContentProperty); - } - return fieldContentMap; + SheetContentPropertyResolver.combineExcelContentProperty(combineExcelContentProperty, excelContentProperty); } /** @@ -276,227 +86,7 @@ private static Map doDeclaredFieldContentMap(Class * @param configurationHolder configuration */ public static FieldCache declaredFields(Class clazz, ConfigurationHolder configurationHolder) { - switch (configurationHolder.globalConfiguration().getFiledCacheLocation()) { - case THREAD_LOCAL: - Map fieldCacheMap = FIELD_THREAD_LOCAL.get(); - if (fieldCacheMap == null) { - fieldCacheMap = MapUtils.newHashMap(); - FIELD_THREAD_LOCAL.set(fieldCacheMap); - } - return fieldCacheMap.computeIfAbsent(new FieldCacheKey(clazz, configurationHolder), key -> { - return doDeclaredFields(clazz, configurationHolder); - }); - case MEMORY: - return FIELD_CACHE.computeIfAbsent(new FieldCacheKey(clazz, configurationHolder), key -> { - return doDeclaredFields(clazz, configurationHolder); - }); - case NONE: - return doDeclaredFields(clazz, configurationHolder); - default: - throw new UnsupportedOperationException("unsupported enum"); - } - } - - private static FieldCache doDeclaredFields(Class clazz, ConfigurationHolder configurationHolder) { - List tempFieldList = FieldUtils.resolveAllFields(clazz); - - ExcelIgnoreUnannotated excelIgnoreUnannotated = clazz.getAnnotation(ExcelIgnoreUnannotated.class); - Set ignoreSet = new HashSet<>(); - // First collect all field names annotated with ExcelIgnore (including subclass overrides) - for (Field field : tempFieldList) { - if (field.getAnnotation(ExcelIgnore.class) != null) { - ignoreSet.add(FieldUtils.resolveCglibFieldName(field)); - } - } - Map> orderFieldMap = new TreeMap<>(); - Map indexFieldMap = new TreeMap<>(); - for (Field field : tempFieldList) { - String fieldName = FieldUtils.resolveCglibFieldName(field); - // Skip if ignored - if (ignoreSet.contains(fieldName)) { - continue; - } - declaredOneField(field, orderFieldMap, indexFieldMap, ignoreSet, excelIgnoreUnannotated); - } - Map sortedFieldMap = buildSortedAllFieldMap(orderFieldMap, indexFieldMap); - FieldCache fieldCache = new FieldCache(sortedFieldMap, indexFieldMap); - - if (!(configurationHolder instanceof WriteHolder)) { - return fieldCache; - } - - WriteHolder writeHolder = (WriteHolder) configurationHolder; - - boolean needIgnore = !CollectionUtils.isEmpty(writeHolder.excludeColumnFieldNames()) - || !CollectionUtils.isEmpty(writeHolder.excludeColumnIndexes()) - || !CollectionUtils.isEmpty(writeHolder.includeColumnFieldNames()) - || !CollectionUtils.isEmpty(writeHolder.includeColumnIndexes()); - - if (!needIgnore) { - return fieldCache; - } - // ignore filed - Map tempSortedFieldMap = MapUtils.newHashMap(); - int index = 0; - for (Map.Entry entry : sortedFieldMap.entrySet()) { - Integer key = entry.getKey(); - FieldWrapper field = entry.getValue(); - - // The current field needs to be ignored - if (writeHolder.ignore(field.getFieldName(), entry.getKey())) { - ignoreSet.add(field.getFieldName()); - // indexFieldMap is keyed by the field's explicit @ExcelProperty(index), which for - // explicit-index fields equals the sortedFieldMap position (entry.getKey()); remove - // by that key, not the running counter, otherwise an unrelated explicit-index entry - // is dropped and the ignored field's entry may survive. - indexFieldMap.remove(key); - } else { - // Mandatory sorted fields - if (indexFieldMap.containsKey(key)) { - tempSortedFieldMap.put(key, field); - } else { - // Need to reorder automatically - // Check whether the current key is already in use - while (tempSortedFieldMap.containsKey(index)) { - index++; - } - tempSortedFieldMap.put(index++, field); - } - } - } - fieldCache.setSortedFieldMap(tempSortedFieldMap); - - // resort field - resortField(writeHolder, fieldCache); - return fieldCache; - } - - /** - * it only works when {@link WriteHolder#includeColumnFieldNames()} or - * {@link WriteHolder#includeColumnIndexes()} has value - * and {@link WriteHolder#orderByIncludeColumn()} is true - **/ - private static void resortField(WriteHolder writeHolder, FieldCache fieldCache) { - if (!writeHolder.orderByIncludeColumn()) { - return; - } - Map indexFieldMap = fieldCache.getIndexFieldMap(); - - Collection includeColumnFieldNames = writeHolder.includeColumnFieldNames(); - if (!CollectionUtils.isEmpty(includeColumnFieldNames)) { - // Field sorted map - Map filedIndexMap = MapUtils.newHashMap(); - int fieldIndex = 0; - for (String includeColumnFieldName : includeColumnFieldNames) { - filedIndexMap.put(includeColumnFieldName, fieldIndex++); - } - - // rebuild sortedFieldMap - Map tempSortedFieldMap = MapUtils.newHashMap(); - fieldCache.getSortedFieldMap().forEach((index, field) -> { - Integer tempFieldIndex = filedIndexMap.get(field.getFieldName()); - if (tempFieldIndex != null) { - tempSortedFieldMap.put(tempFieldIndex, field); - - // The user has redefined the ordering and the ordering of annotations needs to be invalidated - if (!tempFieldIndex.equals(index)) { - indexFieldMap.remove(index); - } - } - }); - fieldCache.setSortedFieldMap(tempSortedFieldMap); - return; - } - - Collection includeColumnIndexes = writeHolder.includeColumnIndexes(); - if (!CollectionUtils.isEmpty(includeColumnIndexes)) { - // Index sorted map - Map filedIndexMap = MapUtils.newHashMap(); - int fieldIndex = 0; - for (Integer includeColumnIndex : includeColumnIndexes) { - filedIndexMap.put(includeColumnIndex, fieldIndex++); - } - - // rebuild sortedFieldMap - Map tempSortedFieldMap = MapUtils.newHashMap(); - fieldCache.getSortedFieldMap().forEach((index, field) -> { - Integer tempFieldIndex = filedIndexMap.get(index); - - // The user has redefined the ordering and the ordering of annotations needs to be invalidated - if (tempFieldIndex != null) { - tempSortedFieldMap.put(tempFieldIndex, field); - } - }); - fieldCache.setSortedFieldMap(tempSortedFieldMap); - } - } - - private static Map buildSortedAllFieldMap( - Map> orderFieldMap, Map indexFieldMap) { - - Map sortedAllFieldMap = - new HashMap<>((orderFieldMap.size() + indexFieldMap.size()) * 4 / 3 + 1); - - Map tempIndexFieldMap = new HashMap<>(indexFieldMap); - int index = 0; - for (List fieldList : orderFieldMap.values()) { - for (FieldWrapper field : fieldList) { - while (tempIndexFieldMap.containsKey(index)) { - sortedAllFieldMap.put(index, tempIndexFieldMap.get(index)); - tempIndexFieldMap.remove(index); - index++; - } - sortedAllFieldMap.put(index, field); - index++; - } - } - sortedAllFieldMap.putAll(tempIndexFieldMap); - return sortedAllFieldMap; - } - - private static void declaredOneField( - Field field, - Map> orderFieldMap, - Map indexFieldMap, - Set ignoreSet, - ExcelIgnoreUnannotated excelIgnoreUnannotated) { - String fieldName = FieldUtils.resolveCglibFieldName(field); - // skip if the field is in ignoreSet - if (ignoreSet.contains(fieldName)) { - return; - } - FieldWrapper fieldWrapper = new FieldWrapper(); - fieldWrapper.setField(field); - fieldWrapper.setFieldName(fieldName); - - ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); - boolean noExcelProperty = excelProperty == null && excelIgnoreUnannotated != null; - boolean isStaticFinalOrTransient = - (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) - || Modifier.isTransient(field.getModifiers()); - if (noExcelProperty || (excelProperty == null && isStaticFinalOrTransient)) { - ignoreSet.add(fieldName); - return; - } - // set heads - if (excelProperty != null) { - fieldWrapper.setHeads(excelProperty.value()); - } - if (excelProperty != null && excelProperty.index() >= 0) { - if (indexFieldMap.containsKey(excelProperty.index())) { - throw new ExcelCommonException("The index of '" - + indexFieldMap.get(excelProperty.index()).getFieldName() + "' and '" + field.getName() - + "' must be inconsistent"); - } - indexFieldMap.put(excelProperty.index(), fieldWrapper); - return; - } - int order = Integer.MAX_VALUE; - if (excelProperty != null) { - order = excelProperty.order(); - } - List orderFieldList = orderFieldMap.computeIfAbsent(order, key -> ListUtils.newArrayList()); - orderFieldList.add(fieldWrapper); + return SheetHeadFieldResolver.resolve(clazz, configurationHolder); } /** @@ -574,8 +164,7 @@ public static class FieldCacheKey { } public static void removeThreadLocalCache() { - FIELD_THREAD_LOCAL.remove(); - CLASS_CONTENT_THREAD_LOCAL.remove(); - CONTENT_THREAD_LOCAL.remove(); + SheetHeadFieldResolver.removeThreadLocalCache(); + SheetContentPropertyResolver.removeThreadLocalCache(); } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java new file mode 100644 index 000000000..eb5e8d36c --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -0,0 +1,114 @@ +/* + * 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.Collections; +import java.util.EnumMap; +import java.util.Map; +import java.util.function.Function; +import org.apache.fesod.common.util.MapUtils; +import org.apache.fesod.sheet.enums.CacheLocationEnum; + +/** + * Where one kind of metadata is cached, one implementation per {@link CacheLocationEnum} constant. + * + * @param the cache key + * @param the cached metadata + */ +interface MetadataCacheStrategy { + + V get(K key, Function mappingFunction); + + /** + * The strategy for every {@link CacheLocationEnum} constant, over the two caches that back them. + */ + static Map> byLocation( + Map memoryCache, ThreadLocal> threadLocalCache) { + Map> strategies = new EnumMap<>(CacheLocationEnum.class); + strategies.put(CacheLocationEnum.MEMORY, new MemoryCache<>(memoryCache)); + strategies.put(CacheLocationEnum.THREAD_LOCAL, new ThreadLocalCache<>(threadLocalCache)); + strategies.put(CacheLocationEnum.NONE, new NoOpCache<>()); + return Collections.unmodifiableMap(strategies); + } + + /** + * Looks up the strategy configured for {@code cacheLocation}, failing loudly when a + * {@link CacheLocationEnum} constant has no strategy registered for it. + */ + static MetadataCacheStrategy select( + Map> strategies, CacheLocationEnum cacheLocation) { + MetadataCacheStrategy strategy = strategies.get(cacheLocation); + if (strategy == null) { + throw new UnsupportedOperationException("unsupported enum"); + } + return strategy; + } + + /** + * The cache will not be cleared unless the app is stopped. + */ + class MemoryCache implements MetadataCacheStrategy { + + private final Map cache; + + MemoryCache(Map cache) { + this.cache = cache; + } + + @Override + public V get(K key, Function mappingFunction) { + return cache.computeIfAbsent(key, mappingFunction); + } + } + + /** + * The cache will be stored in {@code ThreadLocal}, and will be cleared when the excel read and + * write is completed. + */ + class ThreadLocalCache implements MetadataCacheStrategy { + + private final ThreadLocal> cache; + + ThreadLocalCache(ThreadLocal> cache) { + this.cache = cache; + } + + @Override + public V get(K key, Function mappingFunction) { + Map cacheMap = cache.get(); + if (cacheMap == null) { + cacheMap = MapUtils.newHashMap(); + cache.set(cacheMap); + } + return cacheMap.computeIfAbsent(key, mappingFunction); + } + } + + /** + * No caching.It may lose some of performance. + */ + class NoOpCache implements MetadataCacheStrategy { + + @Override + public V get(K key, Function mappingFunction) { + return mappingFunction.apply(key); + } + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java new file mode 100644 index 000000000..5d32901a3 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java @@ -0,0 +1,214 @@ +/* + * 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.lang.reflect.Field; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.apache.fesod.common.util.MapUtils; +import org.apache.fesod.shaded.cglib.beans.BeanMap; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.annotation.format.DateTimeFormat; +import org.apache.fesod.sheet.annotation.format.NumberFormat; +import org.apache.fesod.sheet.annotation.write.style.ContentFontStyle; +import org.apache.fesod.sheet.annotation.write.style.ContentStyle; +import org.apache.fesod.sheet.converters.AutoConverter; +import org.apache.fesod.sheet.converters.Converter; +import org.apache.fesod.sheet.enums.CacheLocationEnum; +import org.apache.fesod.sheet.exception.ExcelCommonException; +import org.apache.fesod.sheet.metadata.ConfigurationHolder; +import org.apache.fesod.sheet.metadata.property.DateTimeFormatProperty; +import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; +import org.apache.fesod.sheet.metadata.property.FontProperty; +import org.apache.fesod.sheet.metadata.property.NumberFormatProperty; +import org.apache.fesod.sheet.metadata.property.StyleProperty; +import org.apache.fesod.sheet.util.ClassUtils.ContentPropertyKey; + +/** + * Resolves the converter, format and style of a single field, merged from the head class and the + * runtime class of the data. + */ +final class SheetContentPropertyResolver { + + /** + * The cache configuration information for each of the class + */ + private static final ThreadLocal, Map>> CLASS_CONTENT_THREAD_LOCAL = + new ThreadLocal<>(); + + /** + * The cache configuration information for each of the class + */ + private static final ThreadLocal> CONTENT_THREAD_LOCAL = + new ThreadLocal<>(); + + private static final Map> + CONTENT_STRATEGIES = MetadataCacheStrategy.byLocation(ClassUtils.CONTENT_CACHE, CONTENT_THREAD_LOCAL); + + private static final Map, Map>> + CLASS_CONTENT_STRATEGIES = + MetadataCacheStrategy.byLocation(ClassUtils.CLASS_CONTENT_CACHE, CLASS_CONTENT_THREAD_LOCAL); + + private SheetContentPropertyResolver() {} + + /** + * Calculate the configuration information for the class + * + * @param dataMap + * @param headClazz + * @param fieldName + * @return + */ + static ExcelContentProperty resolve( + Map dataMap, Class headClazz, String fieldName, ConfigurationHolder configurationHolder) { + Class clazz = null; + if (dataMap instanceof BeanMap) { + Object bean = ((BeanMap) dataMap).getBean(); + if (bean != null) { + clazz = bean.getClass(); + } + } + return getExcelContentProperty(clazz, headClazz, fieldName, configurationHolder); + } + + static void removeThreadLocalCache() { + CLASS_CONTENT_THREAD_LOCAL.remove(); + CONTENT_THREAD_LOCAL.remove(); + } + + private static ExcelContentProperty getExcelContentProperty( + Class clazz, Class headClass, String fieldName, ConfigurationHolder configurationHolder) { + return MetadataCacheStrategy.select( + CONTENT_STRATEGIES, + configurationHolder.globalConfiguration().getFiledCacheLocation()) + .get( + buildKey(clazz, headClass, fieldName), + key -> doGetExcelContentProperty(clazz, headClass, fieldName, configurationHolder)); + } + + private static ExcelContentProperty doGetExcelContentProperty( + Class clazz, Class headClass, String fieldName, ConfigurationHolder configurationHolder) { + ExcelContentProperty excelContentProperty = Optional.ofNullable( + declaredFieldContentMap(clazz, configurationHolder)) + .map(map -> map.get(fieldName)) + .orElse(null); + ExcelContentProperty headExcelContentProperty = Optional.ofNullable( + declaredFieldContentMap(headClass, configurationHolder)) + .map(map -> map.get(fieldName)) + .orElse(null); + ExcelContentProperty combineExcelContentProperty = new ExcelContentProperty(); + + combineExcelContentProperty(combineExcelContentProperty, headExcelContentProperty); + if (clazz != headClass) { + combineExcelContentProperty(combineExcelContentProperty, excelContentProperty); + } + return combineExcelContentProperty; + } + + static void combineExcelContentProperty( + ExcelContentProperty combineExcelContentProperty, ExcelContentProperty excelContentProperty) { + if (excelContentProperty == null) { + return; + } + if (excelContentProperty.getField() != null) { + combineExcelContentProperty.setField(excelContentProperty.getField()); + } + if (excelContentProperty.getConverter() != null) { + combineExcelContentProperty.setConverter(excelContentProperty.getConverter()); + } + if (excelContentProperty.getDateTimeFormatProperty() != null) { + combineExcelContentProperty.setDateTimeFormatProperty(excelContentProperty.getDateTimeFormatProperty()); + } + if (excelContentProperty.getNumberFormatProperty() != null) { + combineExcelContentProperty.setNumberFormatProperty(excelContentProperty.getNumberFormatProperty()); + } + if (excelContentProperty.getContentStyleProperty() != null) { + combineExcelContentProperty.setContentStyleProperty(excelContentProperty.getContentStyleProperty()); + } + if (excelContentProperty.getContentFontProperty() != null) { + combineExcelContentProperty.setContentFontProperty(excelContentProperty.getContentFontProperty()); + } + } + + private static ContentPropertyKey buildKey(Class clazz, Class headClass, String fieldName) { + return new ContentPropertyKey(clazz, headClass, fieldName); + } + + private static Map declaredFieldContentMap( + Class clazz, ConfigurationHolder configurationHolder) { + if (clazz == null) { + return null; + } + return MetadataCacheStrategy.select( + CLASS_CONTENT_STRATEGIES, + configurationHolder.globalConfiguration().getFiledCacheLocation()) + .get(clazz, key -> doDeclaredFieldContentMap(clazz)); + } + + private static Map doDeclaredFieldContentMap(Class clazz) { + if (clazz == null) { + return null; + } + List tempFieldList = FieldUtils.resolveAllFields(clazz); + + ContentStyle parentContentStyle = clazz.getAnnotation(ContentStyle.class); + ContentFontStyle parentContentFontStyle = clazz.getAnnotation(ContentFontStyle.class); + Map fieldContentMap = MapUtils.newHashMapWithExpectedSize(tempFieldList.size()); + for (Field field : tempFieldList) { + ExcelContentProperty excelContentProperty = new ExcelContentProperty(); + excelContentProperty.setField(field); + + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + if (excelProperty != null) { + Class> convertClazz = excelProperty.converter(); + if (convertClazz != AutoConverter.class) { + try { + Converter converter = + convertClazz.getDeclaredConstructor().newInstance(); + excelContentProperty.setConverter(converter); + } catch (Exception e) { + throw new ExcelCommonException("Can not instance custom converter:" + convertClazz.getName()); + } + } + } + + ContentStyle contentStyle = field.getAnnotation(ContentStyle.class); + if (contentStyle == null) { + contentStyle = parentContentStyle; + } + excelContentProperty.setContentStyleProperty(StyleProperty.build(contentStyle)); + + ContentFontStyle contentFontStyle = field.getAnnotation(ContentFontStyle.class); + if (contentFontStyle == null) { + contentFontStyle = parentContentFontStyle; + } + excelContentProperty.setContentFontProperty(FontProperty.build(contentFontStyle)); + + excelContentProperty.setDateTimeFormatProperty( + DateTimeFormatProperty.build(field.getAnnotation(DateTimeFormat.class))); + excelContentProperty.setNumberFormatProperty( + NumberFormatProperty.build(field.getAnnotation(NumberFormat.class))); + + fieldContentMap.put(field.getName(), excelContentProperty); + } + return fieldContentMap; + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java new file mode 100644 index 000000000..4d680e6fc --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java @@ -0,0 +1,277 @@ +/* + * 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.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.fesod.common.util.ListUtils; +import org.apache.fesod.common.util.MapUtils; +import org.apache.fesod.sheet.annotation.ExcelIgnore; +import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated; +import org.apache.fesod.sheet.annotation.ExcelProperty; +import org.apache.fesod.sheet.enums.CacheLocationEnum; +import org.apache.fesod.sheet.exception.ExcelCommonException; +import org.apache.fesod.sheet.metadata.ConfigurationHolder; +import org.apache.fesod.sheet.metadata.FieldCache; +import org.apache.fesod.sheet.metadata.FieldWrapper; +import org.apache.fesod.sheet.util.ClassUtils.FieldCacheKey; +import org.apache.fesod.sheet.write.metadata.holder.WriteHolder; + +/** + * Resolves which declared fields of a class become spreadsheet columns, and in which order. + */ +final class SheetHeadFieldResolver { + + /** + * thread local cache + */ + private static final ThreadLocal> FIELD_THREAD_LOCAL = new ThreadLocal<>(); + + private static final Map> STRATEGIES = + MetadataCacheStrategy.byLocation(ClassUtils.FIELD_CACHE, FIELD_THREAD_LOCAL); + + private SheetHeadFieldResolver() {} + + /** + * Parsing field in the class + * + * @param clazz Need to parse the class + * @param configurationHolder configuration + */ + static FieldCache resolve(Class clazz, ConfigurationHolder configurationHolder) { + return MetadataCacheStrategy.select( + STRATEGIES, configurationHolder.globalConfiguration().getFiledCacheLocation()) + .get(new FieldCacheKey(clazz, configurationHolder), key -> doResolve(clazz, configurationHolder)); + } + + static void removeThreadLocalCache() { + FIELD_THREAD_LOCAL.remove(); + } + + private static FieldCache doResolve(Class clazz, ConfigurationHolder configurationHolder) { + List tempFieldList = FieldUtils.resolveAllFields(clazz); + + ExcelIgnoreUnannotated excelIgnoreUnannotated = clazz.getAnnotation(ExcelIgnoreUnannotated.class); + Set ignoreSet = new HashSet<>(); + // First collect all field names annotated with ExcelIgnore (including subclass overrides) + for (Field field : tempFieldList) { + if (field.getAnnotation(ExcelIgnore.class) != null) { + ignoreSet.add(FieldUtils.resolveCglibFieldName(field)); + } + } + Map> orderFieldMap = new TreeMap<>(); + Map indexFieldMap = new TreeMap<>(); + for (Field field : tempFieldList) { + String fieldName = FieldUtils.resolveCglibFieldName(field); + // Skip if ignored + if (ignoreSet.contains(fieldName)) { + continue; + } + declaredOneField(field, orderFieldMap, indexFieldMap, ignoreSet, excelIgnoreUnannotated); + } + Map sortedFieldMap = buildSortedAllFieldMap(orderFieldMap, indexFieldMap); + FieldCache fieldCache = new FieldCache(sortedFieldMap, indexFieldMap); + + if (!(configurationHolder instanceof WriteHolder)) { + return fieldCache; + } + + WriteHolder writeHolder = (WriteHolder) configurationHolder; + + boolean needIgnore = !CollectionUtils.isEmpty(writeHolder.excludeColumnFieldNames()) + || !CollectionUtils.isEmpty(writeHolder.excludeColumnIndexes()) + || !CollectionUtils.isEmpty(writeHolder.includeColumnFieldNames()) + || !CollectionUtils.isEmpty(writeHolder.includeColumnIndexes()); + + if (!needIgnore) { + return fieldCache; + } + // ignore filed + Map tempSortedFieldMap = MapUtils.newHashMap(); + int index = 0; + for (Map.Entry entry : sortedFieldMap.entrySet()) { + Integer key = entry.getKey(); + FieldWrapper field = entry.getValue(); + + // The current field needs to be ignored + if (writeHolder.ignore(field.getFieldName(), entry.getKey())) { + ignoreSet.add(field.getFieldName()); + // indexFieldMap is keyed by the field's explicit @ExcelProperty(index), which for + // explicit-index fields equals the sortedFieldMap position (entry.getKey()); remove + // by that key, not the running counter, otherwise an unrelated explicit-index entry + // is dropped and the ignored field's entry may survive. + indexFieldMap.remove(key); + } else { + // Mandatory sorted fields + if (indexFieldMap.containsKey(key)) { + tempSortedFieldMap.put(key, field); + } else { + // Need to reorder automatically + // Check whether the current key is already in use + while (tempSortedFieldMap.containsKey(index)) { + index++; + } + tempSortedFieldMap.put(index++, field); + } + } + } + fieldCache.setSortedFieldMap(tempSortedFieldMap); + + // resort field + resortField(writeHolder, fieldCache); + return fieldCache; + } + + /** + * it only works when {@link WriteHolder#includeColumnFieldNames()} or + * {@link WriteHolder#includeColumnIndexes()} has value + * and {@link WriteHolder#orderByIncludeColumn()} is true + **/ + private static void resortField(WriteHolder writeHolder, FieldCache fieldCache) { + if (!writeHolder.orderByIncludeColumn()) { + return; + } + Map indexFieldMap = fieldCache.getIndexFieldMap(); + + Collection includeColumnFieldNames = writeHolder.includeColumnFieldNames(); + if (!CollectionUtils.isEmpty(includeColumnFieldNames)) { + // Field sorted map + Map filedIndexMap = MapUtils.newHashMap(); + int fieldIndex = 0; + for (String includeColumnFieldName : includeColumnFieldNames) { + filedIndexMap.put(includeColumnFieldName, fieldIndex++); + } + + // rebuild sortedFieldMap + Map tempSortedFieldMap = MapUtils.newHashMap(); + fieldCache.getSortedFieldMap().forEach((index, field) -> { + Integer tempFieldIndex = filedIndexMap.get(field.getFieldName()); + if (tempFieldIndex != null) { + tempSortedFieldMap.put(tempFieldIndex, field); + + // The user has redefined the ordering and the ordering of annotations needs to be invalidated + if (!tempFieldIndex.equals(index)) { + indexFieldMap.remove(index); + } + } + }); + fieldCache.setSortedFieldMap(tempSortedFieldMap); + return; + } + + Collection includeColumnIndexes = writeHolder.includeColumnIndexes(); + if (!CollectionUtils.isEmpty(includeColumnIndexes)) { + // Index sorted map + Map filedIndexMap = MapUtils.newHashMap(); + int fieldIndex = 0; + for (Integer includeColumnIndex : includeColumnIndexes) { + filedIndexMap.put(includeColumnIndex, fieldIndex++); + } + + // rebuild sortedFieldMap + Map tempSortedFieldMap = MapUtils.newHashMap(); + fieldCache.getSortedFieldMap().forEach((index, field) -> { + Integer tempFieldIndex = filedIndexMap.get(index); + + // The user has redefined the ordering and the ordering of annotations needs to be invalidated + if (tempFieldIndex != null) { + tempSortedFieldMap.put(tempFieldIndex, field); + } + }); + fieldCache.setSortedFieldMap(tempSortedFieldMap); + } + } + + private static Map buildSortedAllFieldMap( + Map> orderFieldMap, Map indexFieldMap) { + + Map sortedAllFieldMap = + new HashMap<>((orderFieldMap.size() + indexFieldMap.size()) * 4 / 3 + 1); + + Map tempIndexFieldMap = new HashMap<>(indexFieldMap); + int index = 0; + for (List fieldList : orderFieldMap.values()) { + for (FieldWrapper field : fieldList) { + while (tempIndexFieldMap.containsKey(index)) { + sortedAllFieldMap.put(index, tempIndexFieldMap.get(index)); + tempIndexFieldMap.remove(index); + index++; + } + sortedAllFieldMap.put(index, field); + index++; + } + } + sortedAllFieldMap.putAll(tempIndexFieldMap); + return sortedAllFieldMap; + } + + private static void declaredOneField( + Field field, + Map> orderFieldMap, + Map indexFieldMap, + Set ignoreSet, + ExcelIgnoreUnannotated excelIgnoreUnannotated) { + String fieldName = FieldUtils.resolveCglibFieldName(field); + // skip if the field is in ignoreSet + if (ignoreSet.contains(fieldName)) { + return; + } + FieldWrapper fieldWrapper = new FieldWrapper(); + fieldWrapper.setField(field); + fieldWrapper.setFieldName(fieldName); + + ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class); + boolean noExcelProperty = excelProperty == null && excelIgnoreUnannotated != null; + boolean isStaticFinalOrTransient = + (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) + || Modifier.isTransient(field.getModifiers()); + if (noExcelProperty || (excelProperty == null && isStaticFinalOrTransient)) { + ignoreSet.add(fieldName); + return; + } + // set heads + if (excelProperty != null) { + fieldWrapper.setHeads(excelProperty.value()); + } + if (excelProperty != null && excelProperty.index() >= 0) { + if (indexFieldMap.containsKey(excelProperty.index())) { + throw new ExcelCommonException("The index of '" + + indexFieldMap.get(excelProperty.index()).getFieldName() + "' and '" + field.getName() + + "' must be inconsistent"); + } + indexFieldMap.put(excelProperty.index(), fieldWrapper); + return; + } + int order = Integer.MAX_VALUE; + if (excelProperty != null) { + order = excelProperty.order(); + } + List orderFieldList = orderFieldMap.computeIfAbsent(order, key -> ListUtils.newArrayList()); + orderFieldList.add(fieldWrapper); + } +} diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java index c50ad4ff1..3b7170d13 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java @@ -42,7 +42,6 @@ import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; import org.apache.fesod.sheet.testkit.builders.TestDataBuilder; import org.apache.fesod.sheet.testkit.enums.ExcelFormat; -import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.FieldUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Tag; @@ -57,9 +56,10 @@ public class CacheDataTest extends AbstractExcelTest { @Test void clearsThreadLocalFieldCacheAfterRead() throws Exception { File file07 = createTempFile("cache", ExcelFormat.XLSX); - Field field = FieldUtils.getField(ClassUtils.class, "FIELD_THREAD_LOCAL", true); + Class resolver = Class.forName("org.apache.fesod.sheet.util.SheetHeadFieldResolver"); + Field field = FieldUtils.getField(resolver, "FIELD_THREAD_LOCAL", true); ThreadLocal, FieldCache>> fieldThreadLocal = - (ThreadLocal, FieldCache>>) field.get(ClassUtils.class.newInstance()); + (ThreadLocal, FieldCache>>) field.get(null); Assertions.assertNull(fieldThreadLocal.get()); FesodSheet.write(file07, CacheData.class).sheet().doWrite(TestDataBuilder.cacheData(10)); FesodSheet.read(file07, CacheData.class, new PageReadListener(dataList -> { From d89e3e8b1d94418c3f56b7d78e16c775b47d727a Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sat, 12 Sep 2026 23:01:08 +0300 Subject: [PATCH 02/20] chore: document the new internal helpers --- .../org/apache/fesod/sheet/util/MetadataCacheStrategy.java | 6 +++++- .../fesod/sheet/util/SheetContentPropertyResolver.java | 7 +++++-- .../apache/fesod/sheet/util/SheetHeadFieldResolver.java | 6 +++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index eb5e8d36c..a526ea798 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -27,7 +27,11 @@ import org.apache.fesod.sheet.enums.CacheLocationEnum; /** - * Where one kind of metadata is cached, one implementation per {@link CacheLocationEnum} constant. + * Internal helper used by {@link SheetHeadFieldResolver} and {@link SheetContentPropertyResolver} for + * caching resolved metadata, one implementation per {@link CacheLocationEnum} constant. + *

+ * Not intended for direct use; use {@link ClassUtils} as the primary entry point instead. + *

* * @param the cache key * @param the cached metadata diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java index 5d32901a3..a0a7660de 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java @@ -43,8 +43,11 @@ import org.apache.fesod.sheet.util.ClassUtils.ContentPropertyKey; /** - * Resolves the converter, format and style of a single field, merged from the head class and the - * runtime class of the data. + * Internal helper used by {@link ClassUtils} for resolving the converter, format and style of a + * single field, merged from the head class and the runtime class of the data. + *

+ * Not intended for direct use; use {@link ClassUtils} as the primary entry point instead. + *

*/ final class SheetContentPropertyResolver { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java index 4d680e6fc..fa87a9099 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java @@ -43,7 +43,11 @@ import org.apache.fesod.sheet.write.metadata.holder.WriteHolder; /** - * Resolves which declared fields of a class become spreadsheet columns, and in which order. + * Internal helper used by {@link ClassUtils} for resolving which declared fields of a class become + * spreadsheet columns, and in which order. + *

+ * Not intended for direct use; use {@link ClassUtils} as the primary entry point instead. + *

*/ final class SheetHeadFieldResolver { From da5b743ce00dce7412d27849aa0179dc552fc649 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sat, 12 Sep 2026 23:16:13 +0300 Subject: [PATCH 03/20] chore: rename MemoryCache to InMemoryCache --- .../org/apache/fesod/sheet/util/MetadataCacheStrategy.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index a526ea798..df62cc86e 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -46,7 +46,7 @@ interface MetadataCacheStrategy { static Map> byLocation( Map memoryCache, ThreadLocal> threadLocalCache) { Map> strategies = new EnumMap<>(CacheLocationEnum.class); - strategies.put(CacheLocationEnum.MEMORY, new MemoryCache<>(memoryCache)); + strategies.put(CacheLocationEnum.MEMORY, new InMemoryCache<>(memoryCache)); strategies.put(CacheLocationEnum.THREAD_LOCAL, new ThreadLocalCache<>(threadLocalCache)); strategies.put(CacheLocationEnum.NONE, new NoOpCache<>()); return Collections.unmodifiableMap(strategies); @@ -68,11 +68,11 @@ static MetadataCacheStrategy select( /** * The cache will not be cleared unless the app is stopped. */ - class MemoryCache implements MetadataCacheStrategy { + class InMemoryCache implements MetadataCacheStrategy { private final Map cache; - MemoryCache(Map cache) { + InMemoryCache(Map cache) { this.cache = cache; } From e867ad55896a06cc52838c31189fee62dd173ec8 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sat, 12 Sep 2026 23:38:07 +0300 Subject: [PATCH 04/20] refactor: select the cache tier from the holder --- .../sheet/util/MetadataCacheStrategy.java | 31 +-------- .../fesod/sheet/util/MetadataCaches.java | 64 +++++++++++++++++++ .../util/SheetContentPropertyResolver.java | 25 +++----- .../sheet/util/SheetHeadFieldResolver.java | 12 ++-- 4 files changed, 81 insertions(+), 51 deletions(-) create mode 100644 fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index df62cc86e..2be3e6f2b 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -19,16 +19,14 @@ package org.apache.fesod.sheet.util; -import java.util.Collections; -import java.util.EnumMap; import java.util.Map; import java.util.function.Function; import org.apache.fesod.common.util.MapUtils; import org.apache.fesod.sheet.enums.CacheLocationEnum; /** - * Internal helper used by {@link SheetHeadFieldResolver} and {@link SheetContentPropertyResolver} for - * caching resolved metadata, one implementation per {@link CacheLocationEnum} constant. + * Internal helper used by {@link MetadataCaches} for caching resolved metadata, one implementation per + * {@link CacheLocationEnum} constant. *

* Not intended for direct use; use {@link ClassUtils} as the primary entry point instead. *

@@ -40,31 +38,6 @@ interface MetadataCacheStrategy { V get(K key, Function mappingFunction); - /** - * The strategy for every {@link CacheLocationEnum} constant, over the two caches that back them. - */ - static Map> byLocation( - Map memoryCache, ThreadLocal> threadLocalCache) { - Map> strategies = new EnumMap<>(CacheLocationEnum.class); - strategies.put(CacheLocationEnum.MEMORY, new InMemoryCache<>(memoryCache)); - strategies.put(CacheLocationEnum.THREAD_LOCAL, new ThreadLocalCache<>(threadLocalCache)); - strategies.put(CacheLocationEnum.NONE, new NoOpCache<>()); - return Collections.unmodifiableMap(strategies); - } - - /** - * Looks up the strategy configured for {@code cacheLocation}, failing loudly when a - * {@link CacheLocationEnum} constant has no strategy registered for it. - */ - static MetadataCacheStrategy select( - Map> strategies, CacheLocationEnum cacheLocation) { - MetadataCacheStrategy strategy = strategies.get(cacheLocation); - if (strategy == null) { - throw new UnsupportedOperationException("unsupported enum"); - } - return strategy; - } - /** * The cache will not be cleared unless the app is stopped. */ diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java new file mode 100644 index 000000000..d124aabc3 --- /dev/null +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java @@ -0,0 +1,64 @@ +/* + * 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.Collections; +import java.util.EnumMap; +import java.util.Map; +import java.util.function.Function; +import org.apache.fesod.sheet.enums.CacheLocationEnum; +import org.apache.fesod.sheet.metadata.ConfigurationHolder; + +/** + * Internal helper used by {@link SheetHeadFieldResolver} and {@link SheetContentPropertyResolver} for + * caching resolved metadata in the tier the current read or write is configured for. + *

+ * Not intended for direct use; use {@link ClassUtils} as the primary entry point instead. + *

+ * + * @param the cache key + * @param the cached metadata + */ +final class MetadataCaches { + + private final Map> byLocation; + + MetadataCaches(Map memoryCache, ThreadLocal> threadLocalCache) { + Map> strategies = new EnumMap<>(CacheLocationEnum.class); + strategies.put(CacheLocationEnum.MEMORY, new MetadataCacheStrategy.InMemoryCache<>(memoryCache)); + strategies.put(CacheLocationEnum.THREAD_LOCAL, new MetadataCacheStrategy.ThreadLocalCache<>(threadLocalCache)); + strategies.put(CacheLocationEnum.NONE, new MetadataCacheStrategy.NoOpCache<>()); + this.byLocation = Collections.unmodifiableMap(strategies); + } + + /** + * Caches in the tier {@code configurationHolder} is configured for, failing loudly when a + * {@link CacheLocationEnum} constant has no strategy registered for it. + */ + V get(ConfigurationHolder configurationHolder, K key, Function mappingFunction) { + CacheLocationEnum cacheLocation = + configurationHolder.globalConfiguration().getFiledCacheLocation(); + MetadataCacheStrategy strategy = byLocation.get(cacheLocation); + if (strategy == null) { + throw new UnsupportedOperationException("unsupported enum"); + } + return strategy.get(key, mappingFunction); + } +} diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java index a0a7660de..d5febf86e 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java @@ -32,7 +32,6 @@ import org.apache.fesod.sheet.annotation.write.style.ContentStyle; import org.apache.fesod.sheet.converters.AutoConverter; import org.apache.fesod.sheet.converters.Converter; -import org.apache.fesod.sheet.enums.CacheLocationEnum; import org.apache.fesod.sheet.exception.ExcelCommonException; import org.apache.fesod.sheet.metadata.ConfigurationHolder; import org.apache.fesod.sheet.metadata.property.DateTimeFormatProperty; @@ -63,12 +62,11 @@ final class SheetContentPropertyResolver { private static final ThreadLocal> CONTENT_THREAD_LOCAL = new ThreadLocal<>(); - private static final Map> - CONTENT_STRATEGIES = MetadataCacheStrategy.byLocation(ClassUtils.CONTENT_CACHE, CONTENT_THREAD_LOCAL); + private static final MetadataCaches CONTENT_CACHES = + new MetadataCaches<>(ClassUtils.CONTENT_CACHE, CONTENT_THREAD_LOCAL); - private static final Map, Map>> - CLASS_CONTENT_STRATEGIES = - MetadataCacheStrategy.byLocation(ClassUtils.CLASS_CONTENT_CACHE, CLASS_CONTENT_THREAD_LOCAL); + private static final MetadataCaches, Map> CLASS_CONTENT_CACHES = + new MetadataCaches<>(ClassUtils.CLASS_CONTENT_CACHE, CLASS_CONTENT_THREAD_LOCAL); private SheetContentPropertyResolver() {} @@ -99,12 +97,10 @@ static void removeThreadLocalCache() { private static ExcelContentProperty getExcelContentProperty( Class clazz, Class headClass, String fieldName, ConfigurationHolder configurationHolder) { - return MetadataCacheStrategy.select( - CONTENT_STRATEGIES, - configurationHolder.globalConfiguration().getFiledCacheLocation()) - .get( - buildKey(clazz, headClass, fieldName), - key -> doGetExcelContentProperty(clazz, headClass, fieldName, configurationHolder)); + return CONTENT_CACHES.get( + configurationHolder, + buildKey(clazz, headClass, fieldName), + key -> doGetExcelContentProperty(clazz, headClass, fieldName, configurationHolder)); } private static ExcelContentProperty doGetExcelContentProperty( @@ -160,10 +156,7 @@ private static Map declaredFieldContentMap( if (clazz == null) { return null; } - return MetadataCacheStrategy.select( - CLASS_CONTENT_STRATEGIES, - configurationHolder.globalConfiguration().getFiledCacheLocation()) - .get(clazz, key -> doDeclaredFieldContentMap(clazz)); + return CLASS_CONTENT_CACHES.get(configurationHolder, clazz, key -> doDeclaredFieldContentMap(clazz)); } private static Map doDeclaredFieldContentMap(Class clazz) { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java index fa87a9099..89b321630 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java @@ -34,7 +34,6 @@ import org.apache.fesod.sheet.annotation.ExcelIgnore; import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated; import org.apache.fesod.sheet.annotation.ExcelProperty; -import org.apache.fesod.sheet.enums.CacheLocationEnum; import org.apache.fesod.sheet.exception.ExcelCommonException; import org.apache.fesod.sheet.metadata.ConfigurationHolder; import org.apache.fesod.sheet.metadata.FieldCache; @@ -56,8 +55,8 @@ final class SheetHeadFieldResolver { */ private static final ThreadLocal> FIELD_THREAD_LOCAL = new ThreadLocal<>(); - private static final Map> STRATEGIES = - MetadataCacheStrategy.byLocation(ClassUtils.FIELD_CACHE, FIELD_THREAD_LOCAL); + private static final MetadataCaches FIELD_CACHES = + new MetadataCaches<>(ClassUtils.FIELD_CACHE, FIELD_THREAD_LOCAL); private SheetHeadFieldResolver() {} @@ -68,9 +67,10 @@ private SheetHeadFieldResolver() {} * @param configurationHolder configuration */ static FieldCache resolve(Class clazz, ConfigurationHolder configurationHolder) { - return MetadataCacheStrategy.select( - STRATEGIES, configurationHolder.globalConfiguration().getFiledCacheLocation()) - .get(new FieldCacheKey(clazz, configurationHolder), key -> doResolve(clazz, configurationHolder)); + return FIELD_CACHES.get( + configurationHolder, + new FieldCacheKey(clazz, configurationHolder), + key -> doResolve(clazz, configurationHolder)); } static void removeThreadLocalCache() { From f015beab2d7a3896dd35a78d13b82dcab2a5105c Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sat, 12 Sep 2026 23:50:31 +0300 Subject: [PATCH 05/20] refactor: deprecate the public cache fields --- .../apache/fesod/sheet/util/ClassUtils.java | 53 +++++++++++++++++-- .../sheet/util/MetadataCacheStrategy.java | 22 ++++++++ .../fesod/sheet/util/MetadataCaches.java | 36 +++++++++++-- .../util/SheetContentPropertyResolver.java | 36 +++++++------ .../sheet/util/SheetHeadFieldResolver.java | 21 ++++---- .../fesod/sheet/readwrite/CacheDataTest.java | 13 +++-- .../fesod/sheet/util/ClassUtilsTest.java | 23 +++++--- 7 files changed, 158 insertions(+), 46 deletions(-) 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 078e17595..ac67fd276 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 @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -46,20 +47,57 @@ public class ClassUtils { /** * memory cache + * This field is deprecated; use {@link #getFieldCache()} and {@link #removeInMemoryCache()} instead. + *

+ * This field will be removed in future versions. + *

*/ - public static final Map FIELD_CACHE = new ConcurrentHashMap<>(); + @Deprecated + public static final Map FIELD_CACHE = SheetHeadFieldResolver.fieldCache(); /** * The cache configuration information for each of the class + * This field is deprecated; use {@link #getClassContentCache()} and {@link #removeInMemoryCache()} + * instead. + *

+ * This field will be removed in future versions. + *

*/ + @Deprecated public static final ConcurrentHashMap, Map> CLASS_CONTENT_CACHE = - new ConcurrentHashMap<>(); + SheetContentPropertyResolver.classContentCache(); /** * The cache configuration information for each of the class + * This field is deprecated; use {@link #getContentCache()} and {@link #removeInMemoryCache()} instead. + *

+ * This field will be removed in future versions. + *

*/ + @Deprecated public static final ConcurrentHashMap CONTENT_CACHE = - new ConcurrentHashMap<>(); + SheetContentPropertyResolver.contentCache(); + + /** + * An immutable view of the memory cache of parsed fields. + */ + public static Map getFieldCache() { + return Collections.unmodifiableMap(SheetHeadFieldResolver.fieldCache()); + } + + /** + * An immutable view of the memory cache of the configuration information for each of the class. + */ + public static Map, Map> getClassContentCache() { + return Collections.unmodifiableMap(SheetContentPropertyResolver.classContentCache()); + } + + /** + * An immutable view of the memory cache of the configuration information for each of the field. + */ + public static Map getContentCache() { + return Collections.unmodifiableMap(SheetContentPropertyResolver.contentCache()); + } /** * Calculate the configuration information for the class @@ -164,7 +202,12 @@ public static class FieldCacheKey { } public static void removeThreadLocalCache() { - SheetHeadFieldResolver.removeThreadLocalCache(); - SheetContentPropertyResolver.removeThreadLocalCache(); + SheetHeadFieldResolver.clearThreadLocalCache(); + SheetContentPropertyResolver.clearThreadLocalCache(); + } + + public static void removeInMemoryCache() { + SheetHeadFieldResolver.clearInMemoryCache(); + SheetContentPropertyResolver.clearInMemoryCache(); } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index 2be3e6f2b..bc9f76006 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -38,6 +38,13 @@ interface MetadataCacheStrategy { V get(K key, Function mappingFunction); + /** + * Drops the cached entries. {@code ThreadLocal}-backed implementations must detach the entry from + * the thread ({@link ThreadLocal#remove()}) rather than merely empty the map they hold, otherwise + * pooled threads keep the entry forever. + */ + void clear(); + /** * The cache will not be cleared unless the app is stopped. */ @@ -53,6 +60,11 @@ class InMemoryCache implements MetadataCacheStrategy { public V get(K key, Function mappingFunction) { return cache.computeIfAbsent(key, mappingFunction); } + + @Override + public void clear() { + cache.clear(); + } } /** @@ -76,6 +88,11 @@ public V get(K key, Function mappingFunction) { } return cacheMap.computeIfAbsent(key, mappingFunction); } + + @Override + public void clear() { + cache.remove(); + } } /** @@ -87,5 +104,10 @@ class NoOpCache implements MetadataCacheStrategy { public V get(K key, Function mappingFunction) { return mappingFunction.apply(key); } + + @Override + public void clear() { + // nothing is cached + } } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java index d124aabc3..8a708199a 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.EnumMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import org.apache.fesod.sheet.enums.CacheLocationEnum; import org.apache.fesod.sheet.metadata.ConfigurationHolder; @@ -38,9 +39,13 @@ */ final class MetadataCaches { + private final ConcurrentHashMap memoryCache = new ConcurrentHashMap<>(); + + private final ThreadLocal> threadLocalCache = new ThreadLocal<>(); + private final Map> byLocation; - MetadataCaches(Map memoryCache, ThreadLocal> threadLocalCache) { + MetadataCaches() { Map> strategies = new EnumMap<>(CacheLocationEnum.class); strategies.put(CacheLocationEnum.MEMORY, new MetadataCacheStrategy.InMemoryCache<>(memoryCache)); strategies.put(CacheLocationEnum.THREAD_LOCAL, new MetadataCacheStrategy.ThreadLocalCache<>(threadLocalCache)); @@ -49,16 +54,39 @@ final class MetadataCaches { } /** - * Caches in the tier {@code configurationHolder} is configured for, failing loudly when a - * {@link CacheLocationEnum} constant has no strategy registered for it. + * The map backing {@link CacheLocationEnum#MEMORY}, so {@link ClassUtils} can keep publishing it + * without owning it. + */ + ConcurrentHashMap memoryCache() { + return memoryCache; + } + + /** + * Caches in the tier {@code configurationHolder} is configured for. */ V get(ConfigurationHolder configurationHolder, K key, Function mappingFunction) { CacheLocationEnum cacheLocation = configurationHolder.globalConfiguration().getFiledCacheLocation(); + return at(cacheLocation).get(key, mappingFunction); + } + + void clearThreadLocal() { + at(CacheLocationEnum.THREAD_LOCAL).clear(); + } + + void clearInMemory() { + at(CacheLocationEnum.MEMORY).clear(); + } + + /** + * Looks up the strategy configured for {@code cacheLocation}, failing loudly when a + * {@link CacheLocationEnum} constant has no strategy registered for it. + */ + private MetadataCacheStrategy at(CacheLocationEnum cacheLocation) { MetadataCacheStrategy strategy = byLocation.get(cacheLocation); if (strategy == null) { throw new UnsupportedOperationException("unsupported enum"); } - return strategy.get(key, mappingFunction); + return strategy; } } diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java index d5febf86e..ac00fbb73 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import org.apache.fesod.common.util.MapUtils; import org.apache.fesod.shaded.cglib.beans.BeanMap; import org.apache.fesod.sheet.annotation.ExcelProperty; @@ -50,23 +51,11 @@ */ final class SheetContentPropertyResolver { - /** - * The cache configuration information for each of the class - */ - private static final ThreadLocal, Map>> CLASS_CONTENT_THREAD_LOCAL = - new ThreadLocal<>(); - - /** - * The cache configuration information for each of the class - */ - private static final ThreadLocal> CONTENT_THREAD_LOCAL = - new ThreadLocal<>(); - private static final MetadataCaches CONTENT_CACHES = - new MetadataCaches<>(ClassUtils.CONTENT_CACHE, CONTENT_THREAD_LOCAL); + new MetadataCaches<>(); private static final MetadataCaches, Map> CLASS_CONTENT_CACHES = - new MetadataCaches<>(ClassUtils.CLASS_CONTENT_CACHE, CLASS_CONTENT_THREAD_LOCAL); + new MetadataCaches<>(); private SheetContentPropertyResolver() {} @@ -90,9 +79,22 @@ static ExcelContentProperty resolve( return getExcelContentProperty(clazz, headClazz, fieldName, configurationHolder); } - static void removeThreadLocalCache() { - CLASS_CONTENT_THREAD_LOCAL.remove(); - CONTENT_THREAD_LOCAL.remove(); + static ConcurrentHashMap contentCache() { + return CONTENT_CACHES.memoryCache(); + } + + static ConcurrentHashMap, Map> classContentCache() { + return CLASS_CONTENT_CACHES.memoryCache(); + } + + static void clearThreadLocalCache() { + CLASS_CONTENT_CACHES.clearThreadLocal(); + CONTENT_CACHES.clearThreadLocal(); + } + + static void clearInMemoryCache() { + CLASS_CONTENT_CACHES.clearInMemory(); + CONTENT_CACHES.clearInMemory(); } private static ExcelContentProperty getExcelContentProperty( diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java index 89b321630..b52c0a491 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.fesod.common.util.ListUtils; import org.apache.fesod.common.util.MapUtils; @@ -50,13 +51,7 @@ */ final class SheetHeadFieldResolver { - /** - * thread local cache - */ - private static final ThreadLocal> FIELD_THREAD_LOCAL = new ThreadLocal<>(); - - private static final MetadataCaches FIELD_CACHES = - new MetadataCaches<>(ClassUtils.FIELD_CACHE, FIELD_THREAD_LOCAL); + private static final MetadataCaches FIELD_CACHES = new MetadataCaches<>(); private SheetHeadFieldResolver() {} @@ -73,8 +68,16 @@ static FieldCache resolve(Class clazz, ConfigurationHolder configurationHolde key -> doResolve(clazz, configurationHolder)); } - static void removeThreadLocalCache() { - FIELD_THREAD_LOCAL.remove(); + static ConcurrentHashMap fieldCache() { + return FIELD_CACHES.memoryCache(); + } + + static void clearThreadLocalCache() { + FIELD_CACHES.clearThreadLocal(); + } + + static void clearInMemoryCache() { + FIELD_CACHES.clearInMemory(); } private static FieldCache doResolve(Class clazz, ConfigurationHolder configurationHolder) { diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java index 3b7170d13..26ca2f2c7 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java @@ -36,7 +36,6 @@ import org.apache.fesod.sheet.context.AnalysisContext; import org.apache.fesod.sheet.enums.CacheLocationEnum; import org.apache.fesod.sheet.event.AnalysisEventListener; -import org.apache.fesod.sheet.metadata.FieldCache; import org.apache.fesod.sheet.read.listener.PageReadListener; import org.apache.fesod.sheet.testkit.Tags; import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; @@ -56,10 +55,7 @@ public class CacheDataTest extends AbstractExcelTest { @Test void clearsThreadLocalFieldCacheAfterRead() throws Exception { File file07 = createTempFile("cache", ExcelFormat.XLSX); - Class resolver = Class.forName("org.apache.fesod.sheet.util.SheetHeadFieldResolver"); - Field field = FieldUtils.getField(resolver, "FIELD_THREAD_LOCAL", true); - ThreadLocal, FieldCache>> fieldThreadLocal = - (ThreadLocal, FieldCache>>) field.get(null); + ThreadLocal fieldThreadLocal = headFieldThreadLocal(); Assertions.assertNull(fieldThreadLocal.get()); FesodSheet.write(file07, CacheData.class).sheet().doWrite(TestDataBuilder.cacheData(10)); FesodSheet.read(file07, CacheData.class, new PageReadListener(dataList -> { @@ -70,6 +66,13 @@ void clearsThreadLocalFieldCacheAfterRead() throws Exception { Assertions.assertNull(fieldThreadLocal.get()); } + private static ThreadLocal headFieldThreadLocal() throws Exception { + Class resolver = Class.forName("org.apache.fesod.sheet.util.SheetHeadFieldResolver"); + Object caches = FieldUtils.getField(resolver, "FIELD_CACHES", true).get(null); + Field threadLocalCache = FieldUtils.getField(caches.getClass(), "threadLocalCache", true); + return (ThreadLocal) threadLocalCache.get(caches); + } + @Test void usesUpdatedHeaderWithInvocationCache() throws Exception { setNameHeader("Name"); diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java index 18664737a..f8a9099b2 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java @@ -74,9 +74,7 @@ void setUp() { @AfterEach void tearDown() { ClassUtils.removeThreadLocalCache(); - ClassUtils.FIELD_CACHE.clear(); - ClassUtils.CONTENT_CACHE.clear(); - ClassUtils.CLASS_CONTENT_CACHE.clear(); + ClassUtils.removeInMemoryCache(); } private static class SimpleEntity { @@ -121,12 +119,25 @@ void test_declaredFields_cache_memory() { FieldCache cache1 = ClassUtils.declaredFields(SimpleEntity.class, writeHolder); Assertions.assertNotNull(cache1); - Assertions.assertFalse(ClassUtils.FIELD_CACHE.isEmpty()); + Assertions.assertFalse(ClassUtils.getFieldCache().isEmpty()); FieldCache cache2 = ClassUtils.declaredFields(SimpleEntity.class, writeHolder); Assertions.assertSame(cache1, cache2); } + @Test + void test_getFieldCache_immutableViewOfTheLiveCache() { + Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.MEMORY); + ClassUtils.declaredFields(SimpleEntity.class, writeHolder); + + Map fieldCache = ClassUtils.getFieldCache(); + Assertions.assertFalse(fieldCache.isEmpty()); + Assertions.assertThrows(UnsupportedOperationException.class, fieldCache::clear); + + ClassUtils.removeInMemoryCache(); + Assertions.assertTrue(fieldCache.isEmpty()); + } + @Test void test_declaredFields_cache_ThreadLocal() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.THREAD_LOCAL); @@ -134,7 +145,7 @@ void test_declaredFields_cache_ThreadLocal() { FieldCache cache1 = ClassUtils.declaredFields(SimpleEntity.class, writeHolder); Assertions.assertNotNull(cache1); - Assertions.assertTrue(ClassUtils.FIELD_CACHE.isEmpty()); + Assertions.assertTrue(ClassUtils.getFieldCache().isEmpty()); FieldCache cache2 = ClassUtils.declaredFields(SimpleEntity.class, writeHolder); Assertions.assertSame(cache1, cache2); @@ -148,7 +159,7 @@ void test_declaredFields_non_cache() { FieldCache cache2 = ClassUtils.declaredFields(SimpleEntity.class, writeHolder); Assertions.assertNotSame(cache1, cache2); - Assertions.assertTrue(ClassUtils.FIELD_CACHE.isEmpty()); + Assertions.assertTrue(ClassUtils.getFieldCache().isEmpty()); } @Test From f2474f0011de0348d7d25870f736ad4b9f88d319 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 19:37:00 +0300 Subject: [PATCH 06/20] chore: improve comments --- .../main/java/org/apache/fesod/sheet/util/ClassUtils.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 ac67fd276..d25601e9d 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 @@ -47,7 +47,9 @@ public class ClassUtils { /** * memory cache + *

* This field is deprecated; use {@link #getFieldCache()} and {@link #removeInMemoryCache()} instead. + *

*

* This field will be removed in future versions. *

@@ -57,8 +59,10 @@ public class ClassUtils { /** * The cache configuration information for each of the class + *

* This field is deprecated; use {@link #getClassContentCache()} and {@link #removeInMemoryCache()} * instead. + *

*

* This field will be removed in future versions. *

@@ -69,7 +73,9 @@ public class ClassUtils { /** * The cache configuration information for each of the class + *

* This field is deprecated; use {@link #getContentCache()} and {@link #removeInMemoryCache()} instead. + *

*

* This field will be removed in future versions. *

From d60781f857f43994d5153fcf4287eaea9ee40b8e Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 20:49:52 +0300 Subject: [PATCH 07/20] refactor: let each cache tier own its storage --- .../apache/fesod/sheet/util/ClassUtils.java | 7 ++-- .../sheet/util/MetadataCacheStrategy.java | 37 ++++++++++++++++--- .../fesod/sheet/util/MetadataCaches.java | 23 +++++++----- .../util/SheetContentPropertyResolver.java | 18 ++++++++- .../sheet/util/SheetHeadFieldResolver.java | 9 ++++- .../fesod/sheet/readwrite/CacheDataTest.java | 7 +++- 6 files changed, 78 insertions(+), 23 deletions(-) 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 d25601e9d..505bfa323 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 @@ -27,7 +27,6 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -88,21 +87,21 @@ public class ClassUtils { * An immutable view of the memory cache of parsed fields. */ public static Map getFieldCache() { - return Collections.unmodifiableMap(SheetHeadFieldResolver.fieldCache()); + return SheetHeadFieldResolver.fieldCacheView(); } /** * An immutable view of the memory cache of the configuration information for each of the class. */ public static Map, Map> getClassContentCache() { - return Collections.unmodifiableMap(SheetContentPropertyResolver.classContentCache()); + return SheetContentPropertyResolver.classContentCacheView(); } /** * An immutable view of the memory cache of the configuration information for each of the field. */ public static Map getContentCache() { - return Collections.unmodifiableMap(SheetContentPropertyResolver.contentCache()); + return SheetContentPropertyResolver.contentCacheView(); } /** diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index bc9f76006..d2114cc43 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -19,7 +19,9 @@ package org.apache.fesod.sheet.util; +import java.util.Collections; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import org.apache.fesod.common.util.MapUtils; import org.apache.fesod.sheet.enums.CacheLocationEnum; @@ -52,6 +54,14 @@ class InMemoryCache implements MetadataCacheStrategy { private final Map cache; + InMemoryCache() { + this(new ConcurrentHashMap<>()); + } + + /** + * Backs this cache with {@code cache}, to allow map types other than the default + * {@link ConcurrentHashMap}. + */ InMemoryCache(Map cache) { this.cache = cache; } @@ -65,6 +75,27 @@ public V get(K key, Function mappingFunction) { public void clear() { cache.clear(); } + + /** + * Read-only view of the cached entries. + */ + Map view() { + return Collections.unmodifiableMap(cache); + } + + /** + * The live map, only for the deprecated public cache fields on {@link ClassUtils}; remove with them. + * Those fields are declared as {@link ConcurrentHashMap}, so while they exist this cache must be + * backed by one. + */ + ConcurrentHashMap backingMap() { + if (!(cache instanceof ConcurrentHashMap)) { + throw new IllegalStateException( + "The deprecated ClassUtils cache fields require a ConcurrentHashMap, but got " + + cache.getClass().getName()); + } + return (ConcurrentHashMap) cache; + } } /** @@ -73,11 +104,7 @@ public void clear() { */ class ThreadLocalCache implements MetadataCacheStrategy { - private final ThreadLocal> cache; - - ThreadLocalCache(ThreadLocal> cache) { - this.cache = cache; - } + private final ThreadLocal> cache = new ThreadLocal<>(); @Override public V get(K key, Function mappingFunction) { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java index 8a708199a..b4758be97 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java @@ -39,26 +39,31 @@ */ final class MetadataCaches { - private final ConcurrentHashMap memoryCache = new ConcurrentHashMap<>(); - - private final ThreadLocal> threadLocalCache = new ThreadLocal<>(); + private final MetadataCacheStrategy.InMemoryCache inMemoryCache = new MetadataCacheStrategy.InMemoryCache<>(); private final Map> byLocation; MetadataCaches() { Map> strategies = new EnumMap<>(CacheLocationEnum.class); - strategies.put(CacheLocationEnum.MEMORY, new MetadataCacheStrategy.InMemoryCache<>(memoryCache)); - strategies.put(CacheLocationEnum.THREAD_LOCAL, new MetadataCacheStrategy.ThreadLocalCache<>(threadLocalCache)); + strategies.put(CacheLocationEnum.MEMORY, inMemoryCache); + strategies.put(CacheLocationEnum.THREAD_LOCAL, new MetadataCacheStrategy.ThreadLocalCache<>()); strategies.put(CacheLocationEnum.NONE, new MetadataCacheStrategy.NoOpCache<>()); this.byLocation = Collections.unmodifiableMap(strategies); } /** - * The map backing {@link CacheLocationEnum#MEMORY}, so {@link ClassUtils} can keep publishing it - * without owning it. + * Read-only view of the {@link CacheLocationEnum#MEMORY} cache. + */ + Map memoryView() { + return inMemoryCache.view(); + } + + /** + * The live {@link CacheLocationEnum#MEMORY} map, only for the deprecated public cache fields on + * {@link ClassUtils}; remove with them. */ - ConcurrentHashMap memoryCache() { - return memoryCache; + ConcurrentHashMap memoryBackingMap() { + return inMemoryCache.backingMap(); } /** diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java index ac00fbb73..1152f7609 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java @@ -79,12 +79,26 @@ static ExcelContentProperty resolve( return getExcelContentProperty(clazz, headClazz, fieldName, configurationHolder); } + static Map contentCacheView() { + return CONTENT_CACHES.memoryView(); + } + + static Map, Map> classContentCacheView() { + return CLASS_CONTENT_CACHES.memoryView(); + } + + /** + * Only for the deprecated {@link ClassUtils#CONTENT_CACHE}; remove with it. + */ static ConcurrentHashMap contentCache() { - return CONTENT_CACHES.memoryCache(); + return CONTENT_CACHES.memoryBackingMap(); } + /** + * Only for the deprecated {@link ClassUtils#CLASS_CONTENT_CACHE}; remove with it. + */ static ConcurrentHashMap, Map> classContentCache() { - return CLASS_CONTENT_CACHES.memoryCache(); + return CLASS_CONTENT_CACHES.memoryBackingMap(); } static void clearThreadLocalCache() { diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java index b52c0a491..8e2269e2c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetHeadFieldResolver.java @@ -68,8 +68,15 @@ static FieldCache resolve(Class clazz, ConfigurationHolder configurationHolde key -> doResolve(clazz, configurationHolder)); } + static Map fieldCacheView() { + return FIELD_CACHES.memoryView(); + } + + /** + * Only for the deprecated {@link ClassUtils#FIELD_CACHE}; remove with it. + */ static ConcurrentHashMap fieldCache() { - return FIELD_CACHES.memoryCache(); + return FIELD_CACHES.memoryBackingMap(); } static void clearThreadLocalCache() { diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java index 26ca2f2c7..43ac914cb 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java @@ -69,8 +69,11 @@ void clearsThreadLocalFieldCacheAfterRead() throws Exception { private static ThreadLocal headFieldThreadLocal() throws Exception { Class resolver = Class.forName("org.apache.fesod.sheet.util.SheetHeadFieldResolver"); Object caches = FieldUtils.getField(resolver, "FIELD_CACHES", true).get(null); - Field threadLocalCache = FieldUtils.getField(caches.getClass(), "threadLocalCache", true); - return (ThreadLocal) threadLocalCache.get(caches); + Map byLocation = (Map) + FieldUtils.getField(caches.getClass(), "byLocation", true).get(caches); + Object threadLocalCache = byLocation.get(CacheLocationEnum.THREAD_LOCAL); + return (ThreadLocal) + FieldUtils.getField(threadLocalCache.getClass(), "cache", true).get(threadLocalCache); } @Test From fa7bee1c5cb4de8e28dc779a07f33af852466c2d Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 20:57:12 +0300 Subject: [PATCH 08/20] refactor: let callers pick the thread map type --- .../fesod/sheet/util/MetadataCacheStrategy.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index d2114cc43..88e97a325 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.function.Supplier; import org.apache.fesod.common.util.MapUtils; import org.apache.fesod.sheet.enums.CacheLocationEnum; @@ -106,11 +107,25 @@ class ThreadLocalCache implements MetadataCacheStrategy { private final ThreadLocal> cache = new ThreadLocal<>(); + private final Supplier> mapFactory; + + ThreadLocalCache() { + this(MapUtils::newHashMap); + } + + /** + * Creates each thread's map with {@code mapFactory}, to allow map types other than the default + * {@link java.util.HashMap}. The factory must return a new map on every call. + */ + ThreadLocalCache(Supplier> mapFactory) { + this.mapFactory = mapFactory; + } + @Override public V get(K key, Function mappingFunction) { Map cacheMap = cache.get(); if (cacheMap == null) { - cacheMap = MapUtils.newHashMap(); + cacheMap = mapFactory.get(); cache.set(cacheMap); } return cacheMap.computeIfAbsent(key, mappingFunction); From 8cb236f3bfdff1e7f6c727bb86e359b6f4c6850d Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 21:21:42 +0300 Subject: [PATCH 09/20] chore: clarify the cache view contracts --- .../org/apache/fesod/sheet/util/ClassUtils.java | 14 +++++++++++--- .../fesod/sheet/util/MetadataCacheStrategy.java | 4 ++-- .../apache/fesod/sheet/util/ClassUtilsTest.java | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) 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 505bfa323..90697e907 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 @@ -37,6 +37,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; +import org.apache.fesod.sheet.enums.CacheLocationEnum; import org.apache.fesod.sheet.metadata.ConfigurationHolder; import org.apache.fesod.sheet.metadata.FieldCache; import org.apache.fesod.sheet.metadata.property.ExcelContentProperty; @@ -84,21 +85,24 @@ public class ClassUtils { SheetContentPropertyResolver.contentCache(); /** - * An immutable view of the memory cache of parsed fields. + * An unmodifiable, live view of the memory cache of parsed fields. + * The cached values are shared and must not be modified. */ public static Map getFieldCache() { return SheetHeadFieldResolver.fieldCacheView(); } /** - * An immutable view of the memory cache of the configuration information for each of the class. + * An unmodifiable, live view of the memory cache of the configuration information for each of the class. + * The inner maps and their values are shared and must not be modified. */ public static Map, Map> getClassContentCache() { return SheetContentPropertyResolver.classContentCacheView(); } /** - * An immutable view of the memory cache of the configuration information for each of the field. + * An unmodifiable, live view of the memory cache of the configuration information for each of the field. + * The cached values are shared and must not be modified. */ public static Map getContentCache() { return SheetContentPropertyResolver.contentCacheView(); @@ -211,6 +215,10 @@ public static void removeThreadLocalCache() { SheetContentPropertyResolver.clearThreadLocalCache(); } + /** + * Clears the {@link CacheLocationEnum#MEMORY} caches of parsed fields and content properties. + * The {@code ThreadLocal} caches are left untouched; use {@link #removeThreadLocalCache()} for them. + */ public static void removeInMemoryCache() { SheetHeadFieldResolver.clearInMemoryCache(); SheetContentPropertyResolver.clearInMemoryCache(); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index 88e97a325..bd396a6ab 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -49,7 +49,7 @@ interface MetadataCacheStrategy { void clear(); /** - * The cache will not be cleared unless the app is stopped. + * The cache is kept until the app is stopped or {@link #clear()} is called. */ class InMemoryCache implements MetadataCacheStrategy { @@ -138,7 +138,7 @@ public void clear() { } /** - * No caching.It may lose some of performance. + * No caching. It may lose some of performance. */ class NoOpCache implements MetadataCacheStrategy { diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java index f8a9099b2..10c0828f5 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java @@ -126,7 +126,7 @@ void test_declaredFields_cache_memory() { } @Test - void test_getFieldCache_immutableViewOfTheLiveCache() { + void test_getFieldCache_unmodifiableViewOfTheLiveCache() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.MEMORY); ClassUtils.declaredFields(SimpleEntity.class, writeHolder); From 90ee3f5d0d58b4d5fe12354cd038e10818920674 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 21:24:38 +0300 Subject: [PATCH 10/20] test: cover the content cache views --- .../fesod/sheet/util/ClassUtilsTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java index 10c0828f5..9d9ac1132 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/ClassUtilsTest.java @@ -138,6 +138,40 @@ void test_getFieldCache_unmodifiableViewOfTheLiveCache() { Assertions.assertTrue(fieldCache.isEmpty()); } + @Test + void test_getClassContentCache_unmodifiableViewOfTheLiveCache() { + Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.MEMORY); + ClassUtils.declaredExcelContentProperty(null, FormatEntity.class, "date", writeHolder); + + Map, Map> classContentCache = ClassUtils.getClassContentCache(); + Assertions.assertEquals( + "yyyy-MM-dd", + classContentCache + .get(FormatEntity.class) + .get("date") + .getDateTimeFormatProperty() + .getFormat()); + Assertions.assertThrows(UnsupportedOperationException.class, classContentCache::clear); + + ClassUtils.removeInMemoryCache(); + Assertions.assertTrue(classContentCache.isEmpty()); + } + + @Test + void test_getContentCache_unmodifiableViewOfTheLiveCache() { + Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.MEMORY); + ExcelContentProperty property = + ClassUtils.declaredExcelContentProperty(null, FormatEntity.class, "date", writeHolder); + + Map contentCache = ClassUtils.getContentCache(); + Assertions.assertSame( + property, contentCache.get(new ClassUtils.ContentPropertyKey(null, FormatEntity.class, "date"))); + Assertions.assertThrows(UnsupportedOperationException.class, contentCache::clear); + + ClassUtils.removeInMemoryCache(); + Assertions.assertTrue(contentCache.isEmpty()); + } + @Test void test_declaredFields_cache_ThreadLocal() { Mockito.when(globalConfiguration.getFiledCacheLocation()).thenReturn(CacheLocationEnum.THREAD_LOCAL); From 237c809f60f94d4aceb9f53849474eb2ce20b639 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 21:46:33 +0300 Subject: [PATCH 11/20] test: assert thread-local clearing by behaviour --- .../fesod/sheet/readwrite/CacheDataTest.java | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java index 43ac914cb..301f9eae5 100644 --- a/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/readwrite/CacheDataTest.java @@ -36,11 +36,13 @@ import org.apache.fesod.sheet.context.AnalysisContext; import org.apache.fesod.sheet.enums.CacheLocationEnum; import org.apache.fesod.sheet.event.AnalysisEventListener; -import org.apache.fesod.sheet.read.listener.PageReadListener; +import org.apache.fesod.sheet.metadata.FieldCache; +import org.apache.fesod.sheet.read.metadata.holder.ReadHolder; import org.apache.fesod.sheet.testkit.Tags; import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; import org.apache.fesod.sheet.testkit.builders.TestDataBuilder; import org.apache.fesod.sheet.testkit.enums.ExcelFormat; +import org.apache.fesod.sheet.util.ClassUtils; import org.apache.fesod.sheet.util.FieldUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Tag; @@ -55,25 +57,16 @@ public class CacheDataTest extends AbstractExcelTest { @Test void clearsThreadLocalFieldCacheAfterRead() throws Exception { File file07 = createTempFile("cache", ExcelFormat.XLSX); - ThreadLocal fieldThreadLocal = headFieldThreadLocal(); - Assertions.assertNull(fieldThreadLocal.get()); FesodSheet.write(file07, CacheData.class).sheet().doWrite(TestDataBuilder.cacheData(10)); - FesodSheet.read(file07, CacheData.class, new PageReadListener(dataList -> { - Assertions.assertNotNull(fieldThreadLocal.get()); - })) - .sheet() - .doRead(); - Assertions.assertNull(fieldThreadLocal.get()); - } - - private static ThreadLocal headFieldThreadLocal() throws Exception { - Class resolver = Class.forName("org.apache.fesod.sheet.util.SheetHeadFieldResolver"); - Object caches = FieldUtils.getField(resolver, "FIELD_CACHES", true).get(null); - Map byLocation = (Map) - FieldUtils.getField(caches.getClass(), "byLocation", true).get(caches); - Object threadLocalCache = byLocation.get(CacheLocationEnum.THREAD_LOCAL); - return (ThreadLocal) - FieldUtils.getField(threadLocalCache.getClass(), "cache", true).get(threadLocalCache); + FieldCacheCapturingListener listener = new FieldCacheCapturingListener(); + FesodSheet.read(file07, CacheData.class, listener).sheet().doRead(); + try { + Assertions.assertNotNull(listener.getFieldCache()); + Assertions.assertNotSame( + listener.getFieldCache(), ClassUtils.declaredFields(CacheData.class, listener.getReadHolder())); + } finally { + ClassUtils.removeThreadLocalCache(); + } } @Test @@ -155,4 +148,20 @@ public void invoke(CacheData data, AnalysisContext context) {} @Override public void doAfterAllAnalysed(AnalysisContext context) {} } + + @Getter + private static class FieldCacheCapturingListener extends AnalysisEventListener { + private ReadHolder readHolder; + private FieldCache fieldCache; + + @Override + public void invoke(CacheData data, AnalysisContext context) { + readHolder = context.currentReadHolder(); + fieldCache = ClassUtils.declaredFields(CacheData.class, readHolder); + Assertions.assertSame(fieldCache, ClassUtils.declaredFields(CacheData.class, readHolder)); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) {} + } } From bd9b639f2f15d79b964646b354a9c2e2c5314d69 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 21:46:33 +0300 Subject: [PATCH 12/20] refactor: clear the memory tier through its field --- .../main/java/org/apache/fesod/sheet/util/MetadataCaches.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java index b4758be97..63bdddb90 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCaches.java @@ -80,7 +80,7 @@ void clearThreadLocal() { } void clearInMemory() { - at(CacheLocationEnum.MEMORY).clear(); + inMemoryCache.clear(); } /** From f0b8b8aa0ebbe933175b94b27bad32e647503169 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 21:46:33 +0300 Subject: [PATCH 13/20] chore: document the scope of cache clearing --- .../org/apache/fesod/sheet/util/MetadataCacheStrategy.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index bd396a6ab..211b573cf 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -42,7 +42,8 @@ interface MetadataCacheStrategy { V get(K key, Function mappingFunction); /** - * Drops the cached entries. {@code ThreadLocal}-backed implementations must detach the entry from + * Drops the cached entries visible to the calling thread: every entry for a shared cache, only the + * calling thread's own for a per-thread one. {@code ThreadLocal}-backed implementations must detach the entry from * the thread ({@link ThreadLocal#remove()}) rather than merely empty the map they hold, otherwise * pooled threads keep the entry forever. */ From 85d3180e3e7ba3247c35996fd6b5e3415963c108 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 21:49:59 +0300 Subject: [PATCH 14/20] refactor: build the cache view once --- .../org/apache/fesod/sheet/util/MetadataCacheStrategy.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index 211b573cf..6d4a45c07 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -56,6 +56,8 @@ class InMemoryCache implements MetadataCacheStrategy { private final Map cache; + private final Map view; + InMemoryCache() { this(new ConcurrentHashMap<>()); } @@ -66,6 +68,7 @@ class InMemoryCache implements MetadataCacheStrategy { */ InMemoryCache(Map cache) { this.cache = cache; + this.view = Collections.unmodifiableMap(cache); } @Override @@ -82,7 +85,7 @@ public void clear() { * Read-only view of the cached entries. */ Map view() { - return Collections.unmodifiableMap(cache); + return view; } /** From 3eaf78b390da0e43ff54815565c0267771bb1866 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 21:49:59 +0300 Subject: [PATCH 15/20] chore: align the content resolver method names --- .../util/SheetContentPropertyResolver.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java index 1152f7609..47c7536c0 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java @@ -76,7 +76,7 @@ static ExcelContentProperty resolve( clazz = bean.getClass(); } } - return getExcelContentProperty(clazz, headClazz, fieldName, configurationHolder); + return resolve(clazz, headClazz, fieldName, configurationHolder); } static Map contentCacheView() { @@ -111,22 +111,21 @@ static void clearInMemoryCache() { CONTENT_CACHES.clearInMemory(); } - private static ExcelContentProperty getExcelContentProperty( + private static ExcelContentProperty resolve( Class clazz, Class headClass, String fieldName, ConfigurationHolder configurationHolder) { return CONTENT_CACHES.get( configurationHolder, buildKey(clazz, headClass, fieldName), - key -> doGetExcelContentProperty(clazz, headClass, fieldName, configurationHolder)); + key -> doResolve(clazz, headClass, fieldName, configurationHolder)); } - private static ExcelContentProperty doGetExcelContentProperty( + private static ExcelContentProperty doResolve( Class clazz, Class headClass, String fieldName, ConfigurationHolder configurationHolder) { - ExcelContentProperty excelContentProperty = Optional.ofNullable( - declaredFieldContentMap(clazz, configurationHolder)) + ExcelContentProperty excelContentProperty = Optional.ofNullable(resolveClassContent(clazz, configurationHolder)) .map(map -> map.get(fieldName)) .orElse(null); ExcelContentProperty headExcelContentProperty = Optional.ofNullable( - declaredFieldContentMap(headClass, configurationHolder)) + resolveClassContent(headClass, configurationHolder)) .map(map -> map.get(fieldName)) .orElse(null); ExcelContentProperty combineExcelContentProperty = new ExcelContentProperty(); @@ -167,15 +166,15 @@ private static ContentPropertyKey buildKey(Class clazz, Class headClass, S return new ContentPropertyKey(clazz, headClass, fieldName); } - private static Map declaredFieldContentMap( + private static Map resolveClassContent( Class clazz, ConfigurationHolder configurationHolder) { if (clazz == null) { return null; } - return CLASS_CONTENT_CACHES.get(configurationHolder, clazz, key -> doDeclaredFieldContentMap(clazz)); + return CLASS_CONTENT_CACHES.get(configurationHolder, clazz, key -> doResolveClassContent(clazz)); } - private static Map doDeclaredFieldContentMap(Class clazz) { + private static Map doResolveClassContent(Class clazz) { if (clazz == null) { return null; } From 0aa9e8fb8a5ec02ab77c8485aeb1f90711f15d55 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Sun, 13 Sep 2026 21:52:35 +0300 Subject: [PATCH 16/20] test: cover detaching the thread-local cache --- .../sheet/util/MetadataCacheStrategyTest.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/util/MetadataCacheStrategyTest.java diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/MetadataCacheStrategyTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/MetadataCacheStrategyTest.java new file mode 100644 index 000000000..2300f639e --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/util/MetadataCacheStrategyTest.java @@ -0,0 +1,52 @@ +/* + * 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.HashMap; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.fesod.sheet.testkit.Tags; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link MetadataCacheStrategy} + */ +@Tag(Tags.UNIT) +class MetadataCacheStrategyTest { + + @Test + void test_ThreadLocalCache_clear_detachesTheMapFromTheThread() { + AtomicInteger createdMaps = new AtomicInteger(); + MetadataCacheStrategy.ThreadLocalCache cache = + new MetadataCacheStrategy.ThreadLocalCache<>(() -> { + createdMaps.incrementAndGet(); + return new HashMap<>(); + }); + + cache.get("key", key -> "value"); + cache.get("key", key -> "value"); + Assertions.assertEquals(1, createdMaps.get()); + + cache.clear(); + cache.get("key", key -> "value"); + Assertions.assertEquals(2, createdMaps.get()); + } +} From 09a3551193e59605daaf3d792cfc33994e302cd5 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Wed, 16 Sep 2026 14:10:31 +0300 Subject: [PATCH 17/20] refactor: fix the in-memory cache to a concurrent map --- .../sheet/util/MetadataCacheStrategy.java | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index 6d4a45c07..65a60b58b 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -54,22 +54,9 @@ interface MetadataCacheStrategy { */ class InMemoryCache implements MetadataCacheStrategy { - private final Map cache; + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); - private final Map view; - - InMemoryCache() { - this(new ConcurrentHashMap<>()); - } - - /** - * Backs this cache with {@code cache}, to allow map types other than the default - * {@link ConcurrentHashMap}. - */ - InMemoryCache(Map cache) { - this.cache = cache; - this.view = Collections.unmodifiableMap(cache); - } + private final Map view = Collections.unmodifiableMap(cache); @Override public V get(K key, Function mappingFunction) { @@ -90,16 +77,9 @@ Map view() { /** * The live map, only for the deprecated public cache fields on {@link ClassUtils}; remove with them. - * Those fields are declared as {@link ConcurrentHashMap}, so while they exist this cache must be - * backed by one. */ ConcurrentHashMap backingMap() { - if (!(cache instanceof ConcurrentHashMap)) { - throw new IllegalStateException( - "The deprecated ClassUtils cache fields require a ConcurrentHashMap, but got " - + cache.getClass().getName()); - } - return (ConcurrentHashMap) cache; + return cache; } } From fa83af822190f76abff0698cbd1891ffb0bcda32 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Wed, 16 Sep 2026 14:11:02 +0300 Subject: [PATCH 18/20] refactor: simplify the thread-local cache init --- .../fesod/sheet/util/MetadataCacheStrategy.java | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index 65a60b58b..56cc14613 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -89,9 +89,7 @@ ConcurrentHashMap backingMap() { */ class ThreadLocalCache implements MetadataCacheStrategy { - private final ThreadLocal> cache = new ThreadLocal<>(); - - private final Supplier> mapFactory; + private final ThreadLocal> cache; ThreadLocalCache() { this(MapUtils::newHashMap); @@ -102,17 +100,12 @@ class ThreadLocalCache implements MetadataCacheStrategy { * {@link java.util.HashMap}. The factory must return a new map on every call. */ ThreadLocalCache(Supplier> mapFactory) { - this.mapFactory = mapFactory; + this.cache = ThreadLocal.withInitial(mapFactory); } @Override public V get(K key, Function mappingFunction) { - Map cacheMap = cache.get(); - if (cacheMap == null) { - cacheMap = mapFactory.get(); - cache.set(cacheMap); - } - return cacheMap.computeIfAbsent(key, mappingFunction); + return cache.get().computeIfAbsent(key, mappingFunction); } @Override From 26780cc8b4627c0df8139baf10a3c9b7c2cec2ef Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Wed, 16 Sep 2026 14:48:26 +0300 Subject: [PATCH 19/20] chore: clarify the content property map name --- .../sheet/util/SheetContentPropertyResolver.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java index 47c7536c0..9ac217119 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/SheetContentPropertyResolver.java @@ -121,11 +121,12 @@ private static ExcelContentProperty resolve( private static ExcelContentProperty doResolve( Class clazz, Class headClass, String fieldName, ConfigurationHolder configurationHolder) { - ExcelContentProperty excelContentProperty = Optional.ofNullable(resolveClassContent(clazz, configurationHolder)) + ExcelContentProperty excelContentProperty = Optional.ofNullable( + resolveContentPropertyMap(clazz, configurationHolder)) .map(map -> map.get(fieldName)) .orElse(null); ExcelContentProperty headExcelContentProperty = Optional.ofNullable( - resolveClassContent(headClass, configurationHolder)) + resolveContentPropertyMap(headClass, configurationHolder)) .map(map -> map.get(fieldName)) .orElse(null); ExcelContentProperty combineExcelContentProperty = new ExcelContentProperty(); @@ -166,15 +167,15 @@ private static ContentPropertyKey buildKey(Class clazz, Class headClass, S return new ContentPropertyKey(clazz, headClass, fieldName); } - private static Map resolveClassContent( + private static Map resolveContentPropertyMap( Class clazz, ConfigurationHolder configurationHolder) { if (clazz == null) { return null; } - return CLASS_CONTENT_CACHES.get(configurationHolder, clazz, key -> doResolveClassContent(clazz)); + return CLASS_CONTENT_CACHES.get(configurationHolder, clazz, key -> doResolveContentPropertyMap(clazz)); } - private static Map doResolveClassContent(Class clazz) { + private static Map doResolveContentPropertyMap(Class clazz) { if (clazz == null) { return null; } From 8fd1b03cefc63a729e4552f714fec52d5de618a2 Mon Sep 17 00:00:00 2001 From: Nikita Kuprins Date: Wed, 16 Sep 2026 16:08:56 +0300 Subject: [PATCH 20/20] chore: use generic spreadsheet wording in javadoc --- .../org/apache/fesod/sheet/util/MetadataCacheStrategy.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java index 56cc14613..1e6a2170c 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/util/MetadataCacheStrategy.java @@ -84,8 +84,8 @@ ConcurrentHashMap backingMap() { } /** - * The cache will be stored in {@code ThreadLocal}, and will be cleared when the excel read and - * write is completed. + * The cache will be stored in {@code ThreadLocal}, and will be cleared when the spreadsheet + * read and write is completed. */ class ThreadLocalCache implements MetadataCacheStrategy {