From 7de7adb88e612de13fe9ee4333e91931ae1f81bc Mon Sep 17 00:00:00 2001 From: liuhy Date: Tue, 20 Oct 2026 05:20:46 -0700 Subject: [PATCH] fix: reduce memory overhead when loading templates Signed-off-by: liuhy --- .../fesod/sheet/context/WriteContextImpl.java | 36 +- .../write/builder/ExcelWriterBuilder.java | 12 +- .../sheet/write/metadata/WriteWorkbook.java | 7 +- .../metadata/holder/WriteWorkbookHolder.java | 31 +- .../write/TemplateStreamBufferingTest.java | 330 ++++++++++++++++++ website/docs/sheet/fill/fill.md | 11 + .../current/sheet/fill/fill.md | 7 + 7 files changed, 399 insertions(+), 35 deletions(-) create mode 100644 fesod-sheet/src/test/java/org/apache/fesod/sheet/write/TemplateStreamBufferingTest.java diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java index 17fcc7aad..1efbd3565 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/context/WriteContextImpl.java @@ -27,6 +27,8 @@ import java.io.File; import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.util.List; import java.util.Map; @@ -35,6 +37,7 @@ import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; +import org.apache.commons.io.IOUtils; import org.apache.fesod.common.util.ListUtils; import org.apache.fesod.common.util.StringUtils; import org.apache.fesod.sheet.enums.HeaderMergeStrategy; @@ -128,20 +131,37 @@ public WriteContextImpl(WriteWorkbook writeWorkbook) { } initCurrentWorkbookHolder(writeWorkbook); - WorkbookWriteHandlerContext workbookWriteHandlerContext = - WriteHandlerUtils.createWorkbookWriteHandlerContext(this); - WriteHandlerUtils.beforeWorkbookCreate(workbookWriteHandlerContext); - try { - WorkBookUtil.createWorkBook(writeWorkbookHolder); - } catch (Exception e) { - throw new ExcelGenerateException("Create workbook failure", e); + try (InputStream template = writeWorkbookHolder.getTempTemplateInputStream()) { + WorkbookWriteHandlerContext workbookWriteHandlerContext = + WriteHandlerUtils.createWorkbookWriteHandlerContext(this); + WriteHandlerUtils.beforeWorkbookCreate(workbookWriteHandlerContext); + try { + WorkBookUtil.createWorkBook(writeWorkbookHolder); + } catch (Exception e) { + throw new ExcelGenerateException("Create workbook failure", e); + } + WriteHandlerUtils.afterWorkbookCreate(workbookWriteHandlerContext); + } catch (IOException e) { + closeOnInitializationFailure(e); + throw new ExcelGenerateException("Close template failure", e); + } catch (Throwable e) { + closeOnInitializationFailure(e); + throw e; } - WriteHandlerUtils.afterWorkbookCreate(workbookWriteHandlerContext); if (log.isDebugEnabled()) { log.debug("Initialization 'WriteContextImpl' complete"); } } + private void closeOnInitializationFailure(Throwable failure) { + IOUtils.closeQuietly(writeWorkbookHolder.getWorkbook(), e -> recordFailure(failure, e)); + if (writeWorkbookHolder.getAutoCloseStream()) { + IOUtils.closeQuietly(writeWorkbookHolder.getOutputStream(), e -> recordFailure(failure, e)); + } + clearEncrypt03(); + removeThreadLocalCache(); + } + /** * Initializes the current workbook holder with the provided WriteWorkbook. * diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/ExcelWriterBuilder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/ExcelWriterBuilder.java index 56d4a6788..245b1d81d 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/ExcelWriterBuilder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/builder/ExcelWriterBuilder.java @@ -128,8 +128,8 @@ public ExcelWriterBuilder withBom(Boolean withBom) { } /** - * Template file. - * This file is read into memory, excessive cases can lead to OOM. + * Template stream, read through a bounded buffer when the writer is built. + * The parsed workbook still resides in memory, so large templates can lead to OOM. */ public ExcelWriterBuilder withTemplate(InputStream templateInputStream) { writeWorkbook.setTemplateInputStream(templateInputStream); @@ -137,8 +137,8 @@ public ExcelWriterBuilder withTemplate(InputStream templateInputStream) { } /** - * Template file. - * This file is read into memory, excessive cases can lead to OOM. + * Template file, read through a bounded buffer when the writer is built. + * The parsed workbook still resides in memory, so large templates can lead to OOM. */ public ExcelWriterBuilder withTemplate(File templateFile) { writeWorkbook.setTemplateFile(templateFile); @@ -146,8 +146,8 @@ public ExcelWriterBuilder withTemplate(File templateFile) { } /** - * Template file. - * This file is read into memory, excessive cases can lead to OOM. + * Template file, read through a bounded buffer when the writer is built. + * The parsed workbook still resides in memory, so large templates can lead to OOM. */ public ExcelWriterBuilder withTemplate(String pathName) { return withTemplate(new File(pathName)); diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteWorkbook.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteWorkbook.java index 1b96c03b0..4656f2a1b 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteWorkbook.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/WriteWorkbook.java @@ -73,15 +73,16 @@ public class WriteWorkbook extends WriteBasicParameter { private Boolean withBom; /** - * Template input stream + * Template input stream, read through a bounded buffer when the writer is built. + * The parsed workbook still resides in memory. *

* If 'inputStream' and 'file' all not empty, file first */ private InputStream templateInputStream; /** - * Template file. - * This file is read into memory, excessive cases can lead to OOM. + * Template file, read through a bounded buffer when the writer is built. + * The parsed workbook still resides in memory, so large templates can lead to OOM. *

* If 'inputStream' and 'file' all not empty, file first */ diff --git a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteWorkbookHolder.java b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteWorkbookHolder.java index 021dd8523..ef4422c83 100644 --- a/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteWorkbookHolder.java +++ b/fesod-sheet/src/main/java/org/apache/fesod/sheet/write/metadata/holder/WriteWorkbookHolder.java @@ -25,8 +25,9 @@ package org.apache.fesod.sheet.write.metadata.holder; -import java.io.ByteArrayInputStream; +import java.io.BufferedInputStream; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; @@ -40,7 +41,7 @@ import lombok.Setter; import lombok.ToString.Exclude; import lombok.extern.slf4j.Slf4j; -import org.apache.fesod.common.util.IoUtils; +import org.apache.commons.io.input.CloseShieldInputStream; import org.apache.fesod.common.util.MapUtils; import org.apache.fesod.sheet.enums.CellDataTypeEnum; import org.apache.fesod.sheet.enums.HolderEnum; @@ -48,7 +49,6 @@ import org.apache.fesod.sheet.metadata.data.DataFormatData; import org.apache.fesod.sheet.support.ExcelTypeEnum; import org.apache.fesod.sheet.util.DateUtils; -import org.apache.fesod.sheet.util.FileUtils; import org.apache.fesod.sheet.util.StyleUtil; import org.apache.fesod.sheet.write.handler.context.WorkbookWriteHandlerContext; import org.apache.fesod.sheet.write.metadata.WriteWorkbook; @@ -129,9 +129,9 @@ public class WriteWorkbookHolder extends AbstractWriteHolder { */ private File templateFile; /** - * Temporary template file stream. + * Buffered template stream, closed after workbook creation. *

- * A temporary file stream needs to be created in order not to modify the original template file. + * Kept non-null after closing because it also marks the presence of a template. */ private InputStream tempTemplateInputStream; /** @@ -242,7 +242,7 @@ public WriteWorkbookHolder(WriteWorkbook writeWorkbook) { // init handler try { initHandler(writeWorkbook, null); - copyTemplate(); + prepareTemplate(); } catch (IOException e) { closeOutputStreamOnFailure(); throw new ExcelGenerateException("Copy template failure.", e); @@ -289,26 +289,21 @@ private void closeOutputStreamOnFailure() { } } - private void copyTemplate() throws IOException { + private void prepareTemplate() throws IOException { if (writeWorkbook.getTemplateFile() == null && writeWorkbook.getTemplateInputStream() == null) { return; } if (this.excelType == ExcelTypeEnum.CSV) { throw new ExcelGenerateException("csv cannot use template."); } - byte[] templateFileByte = null; if (writeWorkbook.getTemplateFile() != null) { - templateFileByte = FileUtils.readFileToByteArray(writeWorkbook.getTemplateFile()); - } else if (writeWorkbook.getTemplateInputStream() != null) { - try { - templateFileByte = IoUtils.toByteArray(writeWorkbook.getTemplateInputStream()); - } finally { - if (autoCloseStream) { - writeWorkbook.getTemplateInputStream().close(); - } - } + this.tempTemplateInputStream = + new BufferedInputStream(new FileInputStream(writeWorkbook.getTemplateFile())); + } else { + InputStream template = writeWorkbook.getTemplateInputStream(); + this.tempTemplateInputStream = + new BufferedInputStream(autoCloseStream ? template : CloseShieldInputStream.wrap(template)); } - this.tempTemplateInputStream = new ByteArrayInputStream(templateFileByte); } @Override diff --git a/fesod-sheet/src/test/java/org/apache/fesod/sheet/write/TemplateStreamBufferingTest.java b/fesod-sheet/src/test/java/org/apache/fesod/sheet/write/TemplateStreamBufferingTest.java new file mode 100644 index 000000000..4c471e2f1 --- /dev/null +++ b/fesod-sheet/src/test/java/org/apache/fesod/sheet/write/TemplateStreamBufferingTest.java @@ -0,0 +1,330 @@ +/* + * 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.write; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import org.apache.fesod.sheet.ExcelWriter; +import org.apache.fesod.sheet.FesodSheet; +import org.apache.fesod.sheet.testkit.Tags; +import org.apache.fesod.sheet.testkit.base.AbstractExcelTest; +import org.apache.fesod.sheet.testkit.enums.ExcelFormat; +import org.apache.fesod.sheet.testkit.params.ExcelFormatSource; +import org.apache.fesod.sheet.testkit.params.FormatScope; +import org.apache.fesod.sheet.write.builder.ExcelWriterBuilder; +import org.apache.fesod.sheet.write.handler.WorkbookWriteHandler; +import org.apache.fesod.sheet.write.metadata.WriteSheet; +import org.apache.fesod.sheet.write.metadata.WriteWorkbook; +import org.apache.fesod.sheet.write.metadata.fill.FillConfig; +import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@Tag(Tags.WRITE) +class TemplateStreamBufferingTest extends AbstractExcelTest { + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void preparingLargeTemplateDoesNotReadTheEntireInput(boolean autoClose) throws Exception { + GeneratedInputStream source = new GeneratedInputStream(32 * 1024 * 1024); + WriteWorkbook configuration = new WriteWorkbook(); + configuration.setOutputStream(new ByteArrayOutputStream()); + configuration.setTemplateInputStream(source); + configuration.setAutoCloseStream(autoClose); + WriteWorkbookHolder holder = new WriteWorkbookHolder(configuration); + try (InputStream prepared = holder.getTempTemplateInputStream()) { + Assertions.assertEquals(0, source.bytesRead, "preparing a template must not materialize its bytes"); + Assertions.assertEquals(0, prepared.read()); + Assertions.assertTrue(source.bytesRead < 32 * 1024 * 1024, "the first read must use bounded buffering"); + } + } + + @ParameterizedTest + @ExcelFormatSource(FormatScope.BINARY) + void fileAndStreamTemplatesRetainFillBehaviorAndOwnership(ExcelFormat format) throws Exception { + File template = template(format); + byte[] original = Files.readAllBytes(template.toPath()); + for (boolean inMemory : new boolean[] {false, true}) { + File fileOutput = createTempFile("file-template", format); + fill(FesodSheet.write(fileOutput).withTemplate(template).inMemory(inMemory)); + assertFilled(fileOutput); + + for (boolean autoClose : new boolean[] {true, false}) { + File streamOutput = createTempFile("stream-template", format); + try (TrackingInputStream source = new TrackingInputStream(Files.newInputStream(template.toPath()))) { + ExcelWriterBuilder builder = FesodSheet.write(streamOutput) + .withTemplate(source) + .autoCloseStream(autoClose) + .inMemory(inMemory); + try (ExcelWriter writer = builder.build()) { + Assertions.assertEquals(autoClose ? 1 : 0, source.closeCalls); + InputStream prepared = + writer.writeContext().writeWorkbookHolder().getTempTemplateInputStream(); + Assertions.assertNotNull(prepared, "the closed stream still marks a template workbook"); + Assertions.assertThrows(IOException.class, prepared::read); + fill(writer); + } + Assertions.assertEquals(autoClose ? 1 : 0, source.closeCalls); + if (!autoClose) { + Assertions.assertEquals(-1, source.read()); + } + } + assertFilled(streamOutput); + } + } + Assertions.assertArrayEquals(original, Files.readAllBytes(template.toPath())); + } + + @ParameterizedTest + @ExcelFormatSource(FormatScope.BINARY) + void writingAppendsToTheTemplateAfterItsStreamIsClosed(ExcelFormat format) throws Exception { + File template = template(format); + for (boolean inMemory : new boolean[] {false, true}) { + File output = createTempFile("append-template", format); + try (ExcelWriter writer = FesodSheet.write(output) + .withTemplate(template) + .inMemory(inMemory) + .build()) { + writer.write( + Collections.singletonList(Collections.singletonList("appended")), + FesodSheet.writerSheet(0).needHead(false).build()); + } + try (Workbook workbook = WorkbookFactory.create(output)) { + Assertions.assertEquals( + "{.name}", workbook.getSheetAt(0).getRow(0).getCell(0).getStringCellValue()); + Assertions.assertEquals( + "footer", workbook.getSheetAt(0).getRow(2).getCell(0).getStringCellValue()); + Assertions.assertEquals( + "appended", workbook.getSheetAt(0).getRow(3).getCell(0).getStringCellValue()); + Assertions.assertEquals( + "untouched", workbook.getSheetAt(1).getRow(0).getCell(0).getStringCellValue()); + } + } + } + + @ParameterizedTest + @ExcelFormatSource(FormatScope.BINARY) + void parsingFailureHonorsCallerStreamOwnership(ExcelFormat format) throws Exception { + for (boolean autoClose : new boolean[] {true, false}) { + try (TrackingInputStream source = new TrackingInputStream(new ByteArrayInputStream(new byte[128]))) { + TrackingOutputStream output = new TrackingOutputStream(); + Assertions.assertThrows(RuntimeException.class, () -> FesodSheet.write(output) + .excelType(format.toExcelTypeEnum()) + .withTemplate(source) + .autoCloseStream(autoClose) + .build()); + Assertions.assertEquals(autoClose ? 1 : 0, source.closeCalls); + Assertions.assertEquals(autoClose ? 1 : 0, output.closeCalls); + } + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void readingFailureHonorsOutputStreamOwnership(boolean autoClose) throws Exception { + try (TrackingInputStream source = new TrackingInputStream(new InputStream() { + @Override + public int read() throws IOException { + throw new IOException("template read failed"); + } + })) { + TrackingOutputStream output = new TrackingOutputStream(); + RuntimeException failure = Assertions.assertThrows(RuntimeException.class, () -> FesodSheet.write(output) + .withTemplate(source) + .autoCloseStream(autoClose) + .build()); + Assertions.assertNotNull(failure.getCause()); + Assertions.assertEquals(autoClose ? 1 : 0, source.closeCalls); + Assertions.assertEquals(autoClose ? 1 : 0, output.closeCalls); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void callbackFailureClosesOnlyOwnedTemplateStream(boolean autoClose) throws Exception { + File template = template(ExcelFormat.XLSX); + for (boolean beforeCreate : new boolean[] {true, false}) { + try (TrackingInputStream source = new TrackingInputStream(Files.newInputStream(template.toPath()))) { + TrackingOutputStream output = new TrackingOutputStream(); + Assertions.assertThrows(IllegalStateException.class, () -> FesodSheet.write(output) + .withTemplate(source) + .autoCloseStream(autoClose) + .registerWriteHandler(new WorkbookWriteHandler() { + @Override + public void beforeWorkbookCreate() { + if (beforeCreate) { + throw new IllegalStateException("before callback failed"); + } + } + + @Override + public void afterWorkbookCreate(WriteWorkbookHolder holder) { + if (!beforeCreate) { + throw new IllegalStateException("after callback failed"); + } + } + }) + .build()); + Assertions.assertEquals(autoClose ? 1 : 0, source.closeCalls); + Assertions.assertEquals(autoClose ? 1 : 0, output.closeCalls); + } + } + } + + @Test + void callbackErrorRetainsCleanupFailure() throws Exception { + File template = template(ExcelFormat.XLSX); + AssertionError callbackFailure = new AssertionError("callback failed"); + IOException outputFailure = new IOException("output close failed"); + try (TrackingInputStream source = new TrackingInputStream(Files.newInputStream(template.toPath()))) { + TrackingOutputStream output = new TrackingOutputStream() { + @Override + public void close() throws IOException { + super.close(); + throw outputFailure; + } + }; + RuntimeException failure = Assertions.assertThrows(RuntimeException.class, () -> FesodSheet.write(output) + .withTemplate(source) + .registerWriteHandler(new WorkbookWriteHandler() { + @Override + public void beforeWorkbookCreate() { + throw callbackFailure; + } + }) + .build()); + Assertions.assertSame(callbackFailure, failure.getCause()); + Assertions.assertArrayEquals(new Throwable[] {outputFailure}, callbackFailure.getSuppressed()); + Assertions.assertEquals(1, source.closeCalls); + Assertions.assertEquals(1, output.closeCalls); + } + } + + private void fill(ExcelWriterBuilder builder) { + try (ExcelWriter writer = builder.build()) { + fill(writer); + } + } + + private void fill(ExcelWriter writer) { + WriteSheet sheet = FesodSheet.writerSheet(0).build(); + FillConfig config = FillConfig.builder().forceNewRow(true).build(); + writer.fill( + Arrays.asList(Collections.singletonMap("name", "first"), Collections.singletonMap("name", "second")), + config, + sheet); + writer.fill(Collections.singletonList(Collections.singletonMap("name", "third")), config, sheet); + } + + private static void assertFilled(File file) throws Exception { + try (Workbook workbook = WorkbookFactory.create(file)) { + Assertions.assertEquals( + "first", workbook.getSheetAt(0).getRow(0).getCell(0).getStringCellValue()); + Assertions.assertEquals( + "second", workbook.getSheetAt(0).getRow(1).getCell(0).getStringCellValue()); + Assertions.assertEquals( + "third", workbook.getSheetAt(0).getRow(2).getCell(0).getStringCellValue()); + Assertions.assertEquals( + "footer", workbook.getSheetAt(0).getRow(4).getCell(0).getStringCellValue()); + Assertions.assertEquals( + "untouched", workbook.getSheetAt(1).getRow(0).getCell(0).getStringCellValue()); + } + } + + private File template(ExcelFormat format) throws Exception { + File template = createTempFile("template", format); + try (Workbook workbook = format == ExcelFormat.XLS ? new HSSFWorkbook() : new XSSFWorkbook(); + FileOutputStream output = new FileOutputStream(template)) { + workbook.createSheet("Data").createRow(0).createCell(0).setCellValue("{.name}"); + workbook.getSheetAt(0).createRow(2).createCell(0).setCellValue("footer"); + workbook.createSheet("Other").createRow(0).createCell(0).setCellValue("untouched"); + workbook.write(output); + } + return template; + } + + private static class TrackingInputStream extends FilterInputStream { + private int closeCalls; + + TrackingInputStream(InputStream input) { + super(input); + } + + @Override + public void close() throws IOException { + closeCalls++; + super.close(); + } + } + + private static class TrackingOutputStream extends ByteArrayOutputStream { + private int closeCalls; + + @Override + public void close() throws IOException { + closeCalls++; + super.close(); + } + } + + private static class GeneratedInputStream extends InputStream { + private final int size; + private int bytesRead; + + GeneratedInputStream(int size) { + this.size = size; + } + + @Override + public int read() { + if (bytesRead >= size) { + return -1; + } + bytesRead++; + return 0; + } + + @Override + public int read(byte[] buffer, int offset, int length) { + if (bytesRead >= size) { + return -1; + } + int count = Math.min(length, size - bytesRead); + Arrays.fill(buffer, offset, offset + count, (byte) 0); + bytesRead += count; + return count; + } + } +} diff --git a/website/docs/sheet/fill/fill.md b/website/docs/sheet/fill/fill.md index 24ea99aa2..1f1f210c0 100644 --- a/website/docs/sheet/fill/fill.md +++ b/website/docs/sheet/fill/fill.md @@ -24,6 +24,17 @@ title: 'Fill' This section explains how to use Fesod to fill data into files. +## Template Memory Usage + +`withTemplate(...).build()` reads the template through a bounded buffer without first copying the +entire file into a byte array. POI still loads the parsed template workbook into memory; template +filling is not a row-by-row streaming operation, even with `inMemory(false)`. Large templates can +therefore still require substantial heap space. + +Template input streams are consumed during `build()` and closed by default. Use +`autoCloseStream(false)` to keep a caller-provided stream open; streams opened internally for +template files are always closed after initialization. + ## Placeholder Syntax A template marks the cells to fill with `{}` placeholders. What sits inside the braces decides how diff --git a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/fill/fill.md b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/fill/fill.md index d6f8458d7..3059bf733 100644 --- a/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/fill/fill.md +++ b/website/i18n/zh-cn/docusaurus-plugin-content-docs/current/sheet/fill/fill.md @@ -24,6 +24,13 @@ title: '填充' 本章节介绍如何使用 Fesod 来填充数据到文件中。 +## 模板内存开销 + +`withTemplate(...).build()` 通过有界缓冲流读取模板,不会先把整个文件复制到字节数组中。 +POI 仍会将解析后的模板工作簿加载到内存;即使设置 `inMemory(false)`,模板填充也不是逐行读取、逐行写出的流式操作,因此大模板仍可能需要较多堆内存。 + +模板输入流在 `build()` 期间被读取,默认在初始化后关闭。使用 `autoCloseStream(false)` 可保持调用方传入的流打开;Fesod 内部为模板文件打开的流始终会在初始化后关闭。 + ## 占位符语法 模板通过 `{}` 占位符标记需要填充的单元格,大括号里的内容决定了填充方式: