Templates & Modifying Workbooks
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Baseline date: 2026-10-09. This page was written against Apache POI 5.5.1 (the latest release on that date; 6.0 is announced but has not been released, so everything about 6.0 below is expected, not shipped), Java 21 and later, Spring Boot 4.x on Spring Framework 7 and Jakarta EE 11. Every example in this page was compiled and run against POI 5.5.1; the observations marked as such come from those runs, not from the documentation.
The earlier pages of this section build workbooks cell by cell: styles, formats, validations, charts and print setup are all created in code. That is the right choice for a generic export, but it is a poor fit for a report that an analyst or a designer wants to restyle every month. The alternative is to let a person design the layout in Excel once, save it as a template, and let the code do only what code is good at: put the data in. This page explains how to open such a template, fill it, reshape it (add rows, copy or remove sheets), keep its macros, and write the result, using the Bookshelf scenario of the section landing page. It builds on Spreadsheet Usermodel Basics, reuses the style rules of Cell Styles, Fonts & Formats, and relies on Tables, Named Ranges & Pivot Tables for the objects it fills. Word templates are a separate topic, covered in the Word pages of this section.
Why start from a template
A template is an ordinary workbook that already contains everything that is not data. The division of labour is the whole point:
-
The designer owns the look. Fonts, colours, column widths, number formats, the table style, frozen panes, the print area and page setup, conditional formatting, data-validation lists, charts and the company logo all live in the file. A change of brand colour is a new template, not a code change and a release.
-
The code owns the data. It finds the places to fill, writes values and formulas, grows tables and writes the file.
-
The contract between them is a handful of names. Defined names (
ReportTitle,GeneratedOn), a table name (Books) and a prototype row. If the designer keeps those, the code keeps working whatever else changes.
The figure shows the life of one report:
(designed in Excel, kept in the repository)"] --> L["Load
bytes to a private XSSFWorkbook"] D[("Bookshelf data
books, loans")] --> F L --> F["Fill
named cells, table rows,
placeholders, shiftRows"] F --> R["Reshape (optional)
cloneSheet, removeSheetAt"] R --> C["Recalculate flag
setForceFormulaRecalculation"] C --> W["write(OutputStream)"] W --> X["close()
the workbook"] X --> O["books-inventory.xlsx
HTTP response or file"]
The cost of the approach is that a template is code in disguise: it can be edited by someone who does not run the tests. Treat it like a source file: keep it in version control next to the code, write a test that opens it and checks that every name and table the code needs is present, and fail with a clear message when one is missing (the examples below do).
What POI offers for templates, and what it does not
POI has no template engine. There is no fill(template, model) call, no ${...} language, no loop construct and no
mail-merge. What it gives you are the primitives that a hand-written filler is made of:
| Task | POI API | Where it is covered |
|---|---|---|
Open a workbook that already exists |
|
|
Find the cell to fill |
|
|
Fill a table and make it grow |
|
|
Replace text markers |
Your own loop over cells; POI offers no search-and-replace |
|
Make room, or repeat a block |
|
|
Produce one sheet per branch, member or month |
|
|
Keep or inspect VBA macros |
|
|
Stream a large report on top of a template |
|
Opening a template safely
Where the template lives
Keep the template next to the code (src/main/resources/templates/) or, when the business edits it, in object storage
or a configuration directory. A Spring Boot fat jar cannot give you a File for a classpath resource, so the template
is read as a stream in the common case. That is also the safe case, as the next subsection shows.
The File constructor can overwrite your template
The official guide recommends a File over an InputStream because it needs less memory. For a read-only job that is
right. For a template it is a trap. Observed with POI 5.5.1:
| How the workbook is opened | What happens after you change it and call write(otherStream) then close() |
Verdict for templates |
|---|---|---|
|
The package is opened read-write on the original file, and |
Dangerous: use only on a private copy |
|
Opening works, but |
Cannot be filled and written |
|
The whole file is buffered in memory, nothing keeps a link to the original file, |
Safe; costs memory proportional to the template |
|
Changes go to the copy. |
Safe and lighter on memory, at the price of a file copy and cleaning up the copy |
Source: POI Quick Guide, Files vs InputStreams, XSSFWorkbook Javadoc and WorkbookFactory Javadoc. The behaviour table is the result of running these cases against POI 5.5.1.
For a service that produces reports all day, read the template bytes once and open a fresh workbook from them for every
report. A workbook is not thread safe and must never be shared between requests, but the immutable byte[] can be:
package com.bookshelf.office;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/** Reads the template once and hands out a fresh, private workbook for every report. */
public final class InventoryTemplate {
private static final String RESOURCE = "/templates/books-inventory-template.xlsx";
private final byte[] bytes;
public InventoryTemplate() throws IOException {
try (InputStream in = InventoryTemplate.class.getResourceAsStream(RESOURCE)) {
if (in == null) {
throw new FileNotFoundException("Missing template " + RESOURCE);
}
this.bytes = in.readAllBytes();
}
}
/** The caller owns the returned workbook and must close it. */
public XSSFWorkbook open() throws IOException {
return new XSSFWorkbook(new ByteArrayInputStream(bytes));
}
}
Opening from an InputStream here is a deliberate exception to the "prefer a File`" advice of the rest of this
section: the `File constructor would write the changes back into the template on close(), and a read-only package
cannot be written at all (see the table above). If memory matters more than a file copy, copy the template to a
temporary file and open that File instead.
The template is then about as large in memory as the file, per concurrent report. For a template of a few hundred kilobytes that is irrelevant; for a template that carries a large embedded image, multiply it by the number of simultaneous exports when sizing the heap.
Template file types
-
Use an ordinary
.xlsx(or.xlsm, see Macro-enabled templates (.xlsm)) as the template. Excel also saves templates as.xltx(and.xltm), which differ only by the main part’s content type. In a quick test, POI 5.5.1 opened a package re-labelled with the.xltxcontent type without complaint, but the POI documentation does not promise it, and the written file keeps the template content type, so Excel would offer to create a new workbook from it instead of opening it. Keep the template as.xlsxunless you have a reason not to. -
A legacy
.xlstemplate works the same way through thess.usermodelinterfaces (Legacy .xls with HSSF), with the limits of that format. -
An encrypted template needs the password,
WorkbookFactory.create(InputStream, String); the template should not be encrypted in the first place, since it contains no data.
A template for the Bookshelf inventory
The examples that follow use books-inventory-template.xlsx. In real life a person builds it in Excel; the helper
below builds the same file so that the page is reproducible. It has a title cell and a date cell with defined names, a
table named Books made of a header row and one prototype data row that carries the column styles, and a footer two
rows below the table.
package com.bookshelf.office;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataFormat;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Name;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.util.AreaReference;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFTable;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
/** One-off helper: in real life the designer builds this file in Excel. */
public final class BuildInventoryTemplate {
public static void main(String[] args) throws IOException {
Path target = Path.of(args.length > 0 ? args[0] : "books-inventory-template.xlsx");
try (XSSFWorkbook wb = new XSSFWorkbook(); OutputStream out = Files.newOutputStream(target)) {
XSSFSheet sheet = wb.createSheet("Inventory");
DataFormat formats = wb.createDataFormat();
Font big = wb.createFont();
big.setBold(true);
big.setFontHeightInPoints((short) 16);
CellStyle titleStyle = wb.createCellStyle();
titleStyle.setFont(big);
CellStyle dateStyle = wb.createCellStyle();
dateStyle.setDataFormat(formats.getFormat("yyyy-mm-dd"));
CellStyle moneyStyle = wb.createCellStyle();
moneyStyle.setDataFormat(formats.getFormat("#,##0.00"));
// Named cells that the code will fill
sheet.createRow(0).createCell(1).setCellStyle(titleStyle);
sheet.createRow(1).createCell(1).setCellStyle(dateStyle);
defineName(wb, "ReportTitle", "Inventory!$B$1");
defineName(wb, "GeneratedOn", "Inventory!$B$2");
// Header row (index 3) and one prototype data row (index 4) that carries the styles
String[] headers = {"Title", "Author", "Year", "Price", "Stock"};
Row header = sheet.createRow(3);
Row prototype = sheet.createRow(4);
for (int c = 0; c < headers.length; c++) {
header.createCell(c).setCellValue(headers[c]);
prototype.createCell(c).setCellStyle(c == 3 ? moneyStyle : wb.createCellStyle());
}
XSSFTable table = sheet.createTable(new AreaReference("A4:E5", SpreadsheetVersion.EXCEL2007));
table.setName("Books");
table.setDisplayName("Books");
table.getCTTable().addNewTableStyleInfo().setName("TableStyleMedium2");
// Footer, two rows under the table: it must move down when the table grows
sheet.createRow(6).createCell(0).setCellValue("Total stock");
sheet.getRow(6).createCell(4).setCellFormula("SUM(E5:E5)");
sheet.createRow(7).createCell(0).setCellValue("Prices are in EUR");
wb.write(out);
}
}
private static void defineName(XSSFWorkbook wb, String name, String reference) {
Name n = wb.createName();
n.setNameName(name);
n.setRefersToFormula(reference);
}
}
Source: POI Quick Guide, Named Ranges and Named Cells and the official example CreateTable.
Filling named cells
A defined name is the most robust address a template can offer. Cell references (B2) break when the designer inserts
a row above them; a name moves with its cell. The lookup has two steps: ask the workbook for the name, then turn the
formula it refers to into a sheet and a cell.
| Call | Notes |
|---|---|
|
Returns every name with that text: the same name can exist once per sheet scope and once for the workbook. Prefer it to
|
|
The text of the reference, for example |
|
The sheet name of a sheet-scoped name; |
|
Parses |
|
Parses a range such as |
The helper below does the lookup, fails with a message that names the missing name, and creates the row or cell when the template left it out (a styled but empty cell may not exist in the file at all):
/** Resolves a workbook-scoped name that points at one cell and returns that cell. */
private static Cell setNamedCell(XSSFWorkbook wb, String name) {
List<? extends Name> names = wb.getNames(name);
if (names.isEmpty()) {
throw new IllegalStateException("The template defines no name '" + name + "'");
}
CellReference ref = new CellReference(names.get(0).getRefersToFormula());
Sheet sheet = wb.getSheet(ref.getSheetName());
Row row = sheet.getRow(ref.getRow());
if (row == null) {
row = sheet.createRow(ref.getRow());
}
return row.getCell(ref.getCol(), Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
}
Setting the value keeps the cell style that the designer applied (the 16 point bold title, the yyyy-mm-dd date format):
setCellValue changes the content, never the style. Writing a LocalDate with setCellValue(LocalDate) stores a date
serial number and relies on that format to display it; if the template cell is a plain General cell the report shows
a number such as 46304.
Filling a table
An Excel table (XSSFTable) is a named range with a header row, banded styling and, optionally, an auto-filter and a
totals row. It is the best anchor for data that grows: a designer can point a chart, a pivot table or a formula at
Books[Price] and the reference follows the data. The catch is that the table is a separate object in the file. Writing
cells below the last row does not enlarge it; you must update its reference yourself.
| Call | Notes |
|---|---|
|
Finds a table by name across all sheets; |
|
The sheet that holds it. |
|
The current area, including the header row (and the totals row, if there is one). |
|
The same as an |
|
Changes the area. This is how a table grows or shrinks. Verified in the tests: the |
|
Number of rows from the table definition, and a refresh of the internal cell-reference cache after you change the area. |
The recipe has four steps, and the example below follows them in order:
-
Prototype row. The template contains one data row (A5:E5 here) whose cells hold the number formats and styles. Read the style from it; do not hard-code styles in the filler.
-
Make room. If rows below the table must stay below it (a footer, a signature block), move them down with
shiftRowsbefore writing the data, as explained in Moving and copying rows. -
Write the data, row by row, applying the prototype’s cell style to each new cell.
-
Resize the table and rewrite any formula that referred to the old data range.
The fillers on this page take the rows as a small Book record:
package com.bookshelf.office;
public record Book(String title, String author, int year, double price, int stock) {
}
package com.bookshelf.office;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDate;
import java.util.List;
import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Name;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.AreaReference;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFTable;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public final class InventoryFiller {
private final InventoryTemplate template;
public InventoryFiller(InventoryTemplate template) {
this.template = template;
}
public void write(String title, List<Book> books, OutputStream out) throws IOException {
try (XSSFWorkbook wb = template.open()) {
setNamedCell(wb, "ReportTitle").setCellValue(title);
setNamedCell(wb, "GeneratedOn").setCellValue(LocalDate.now());
XSSFTable table = wb.getTable("Books");
XSSFSheet sheet = table.getXSSFSheet();
int firstData = table.getStartRowIndex() + 1;
int lastTemplateData = table.getEndRowIndex();
int footerStart = lastTemplateData + 2; // the template leaves one blank row
int extraRows = Math.max(books.size(), 1) - (lastTemplateData - firstData + 1);
// 1. make room: move the footer (and everything below it) down
if (extraRows > 0) {
sheet.shiftRows(footerStart, sheet.getLastRowNum(), extraRows);
}
// 2. write the data, reusing the styles of the prototype row
Row prototype = sheet.getRow(firstData);
int rowIndex = firstData;
for (Book b : books) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
row = sheet.createRow(rowIndex);
}
put(row, prototype, 0).setCellValue(b.title());
put(row, prototype, 1).setCellValue(b.author());
put(row, prototype, 2).setCellValue(b.year());
put(row, prototype, 3).setCellValue(b.price());
put(row, prototype, 4).setCellValue(b.stock());
rowIndex++;
}
int lastData = Math.max(rowIndex - 1, firstData);
// 3. resize the table: shiftRows and setCellValue never touch it
table.setCellReferences(new AreaReference(
new CellReference(table.getStartRowIndex(), table.getStartColIndex()),
new CellReference(lastData, table.getEndColIndex()),
SpreadsheetVersion.EXCEL2007));
// 4. rewrite the footer formula: its range still points at the template rows
Cell total = sheet.getRow(lastData + 2).getCell(4);
total.setCellFormula("SUM(E" + (firstData + 1) + ":E" + (lastData + 1) + ")");
wb.setForceFormulaRecalculation(true); // Excel recalculates when the file is opened
wb.write(out);
}
}
/** Resolves a workbook-scoped name that points at one cell and returns that cell. */
private static Cell setNamedCell(XSSFWorkbook wb, String name) {
List<? extends Name> names = wb.getNames(name);
if (names.isEmpty()) {
throw new IllegalStateException("The template defines no name '" + name + "'");
}
CellReference ref = new CellReference(names.get(0).getRefersToFormula());
Sheet sheet = wb.getSheet(ref.getSheetName());
Row row = sheet.getRow(ref.getRow());
if (row == null) {
row = sheet.createRow(ref.getRow());
}
return row.getCell(ref.getCol(), Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
}
private static Cell put(Row row, Row prototype, int column) {
Cell cell = row.getCell(column, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
cell.setCellStyle(prototype.getCell(column).getCellStyle());
return cell;
}
}
Calling it from a batch job or a test:
InventoryFiller filler = new InventoryFiller(new InventoryTemplate());
try (OutputStream out = Files.newOutputStream(Path.of("books-inventory.xlsx"))) {
filler.write("Bookshelf inventory", books, out);
}
Source: XSSFTable Javadoc, XSSFWorkbook.getTable, POI Quick Guide, Shift rows and CreateTable example.
Run against four books, the output had the table at A4:E8, the footer pushed from rows 7-8 to rows 10-11 and its formula
rewritten to SUM(E5:E8). Run against an empty list, the table stayed A4:E5 (a table needs at least one data row) and
the footer did not move.
$ unzip -p books-inventory.xlsx xl/tables/table1.xml | grep -o 'ref="[^"]*"'
ref="A4:E8"
Rules for tables
-
Header text and table columns must agree. On write POI refreshes the column names from the header cells. Do not put formulas or duplicate texts in the header row: Excel may report a corrupt file and repair it on open.
-
Do not leave data outside the table when it should be inside. Cells written below the old area but not covered by the new reference are plain cells next to the table.
-
Totals rows. A table with a totals row has the totals row inside its area. If your template uses one, the data rows go between the header and the totals row, so the arithmetic of the example changes: shift from the totals row.
-
Structured references in formulas. A formula such as
SUBTOTAL(109,Books[Stock])is stored as text and Excel evaluates it; POI’s ownFormulaEvaluatorsupports only part of the structured-reference syntax, so do not depend on evaluating it in Java (see Formulas & Evaluation). -
A table in an HSSF
.xlstemplate does not exist: tables are an OOXML feature.
Replacing placeholders in cells
Some templates are not tables at all: a delivery note or a letter-like sheet with text such as
Printed by ${user} on ${when}. POI has no search-and-replace, so you write the loop. The version below visits every
text cell of every sheet, leaves formulas, numbers and blanks alone, keeps unknown markers visible (a typo in the
template should be seen, not silently swallowed) and uses Matcher.quoteReplacement so that a value containing $ or
a backslash is inserted literally.
package com.bookshelf.office;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
/** Replaces ${key} markers inside text cells. Formula cells and non-text cells are left alone. */
public final class PlaceholderReplacer {
private static final Pattern MARKER = Pattern.compile("\\$\\{([A-Za-z0-9_.]+)}");
private PlaceholderReplacer() {
}
public static int replace(Workbook wb, Map<String, String> values) {
int replaced = 0;
for (Sheet sheet : wb) {
for (Row row : sheet) {
for (Cell cell : row) {
if (cell.getCellType() != CellType.STRING) {
continue; // formulas, numbers, blanks
}
String text = cell.getStringCellValue();
if (!text.contains("${")) {
continue;
}
Matcher m = MARKER.matcher(text);
StringBuilder out = new StringBuilder();
while (m.find()) {
String value = values.get(m.group(1));
// unknown keys stay visible instead of silently disappearing
m.appendReplacement(out, Matcher.quoteReplacement(value != null ? value : m.group()));
replaced++;
}
m.appendTail(out);
cell.setCellValue(out.toString()); // plain string: rich-text runs are lost
}
}
}
return replaced;
}
}
Source: POI Quick Guide, Iterate over rows and cells and Cell Javadoc.
Running it on a copy of the Bookshelf template with a footer cell Printed by ${user} on ${when} ${unknown} and the
values user=Ada and when=today$1 replaced three markers and produced Printed by Ada on today$1 ${unknown}: the $1
survived because of quoteReplacement.
Things to know about placeholder replacement:
-
Rich text is flattened. A cell whose characters have different fonts is a rich-text string;
getStringCellValue()returns the text without the formatting, andsetCellValue(String)writes plain text in the cell style. If the designer formats part of a placeholder cell, work on theRichTextStringruns instead, or avoid mixed formatting in cells that contain markers. -
Headers and footers are not cells. Page headers and footers (
&P,&Dcodes included) are reached withsheet.getHeader()andsheet.getFooter()and theirgetLeft(),getCenter(),getRight()setters; the loop above does not see them. See Sheet Layout & Printing. -
Formulas that build text from other cells are better than markers when the value is a number or a date: the format stays under the designer’s control.
-
Do not put user-provided text in a marker position without thinking about formula injection. The replacement writes a string cell, which Excel never evaluates, so a value starting with an equals sign is shown as text. That is the safe behaviour; keep it by never calling
setCellFormulawith user data. The Security page of this section, in a later group, covers the export-side rules.
Moving and copying rows
shiftRows
Sheet.shiftRows(startRow, endRow, n) moves rows by n (negative moves up). It is the tool to make room for data that
the template does not have rows for. A signature with two more flags exists, shiftRows(startRow, endRow, n,
copyRowHeight, resetOriginalRowHeight); the three-argument call shifts the heights too.
What actually moves is the question that matters, because the answer differs between objects. The probe below was run
on an XSSF sheet with a name, formulas, a merged region, a conditional-format range, a data validation, a hyperlink, a
comment and a table, after shiftRows(5, 29, 3) on a sheet whose tracked cell was A11:
| Object that referred to the shifted cell A11 | Result in POI 5.5.1 | Moved with the rows? |
|---|---|---|
Defined name |
|
Yes |
Formula in a shifted row ( |
|
Yes |
Formula in a row that did not move (row 3, |
|
Yes |
Merged region |
|
Yes |
Conditional formatting range |
|
Yes |
Hyperlink on the cell |
Now at row 14 |
Yes |
Cell comment |
Now at row 14, none at the old position |
Yes |
Data validation on |
Still |
No |
Table |
Still |
No |
Source: POI Quick Guide, Shift rows,
Sheet.shiftRows Javadoc
and the official example ShiftRows.
The table is the result of running the probe against POI 5.5.1; it is a regression check for your own workbook, not a
contract. Pictures and charts anchored in the shifted area were not part of the probe: check them in your own template
and, if needed, move their anchors with the ClientAnchor API of
Images, Charts, Comments & Hyperlinks.
Pitfalls:
-
Data validations and tables must be fixed by hand, as in the filler above (
setCellReferences). For a validation, recreate it with the newCellRangeAddressList; see Data Validation & Conditional Formatting. -
Shift before you write. Shifting after writing is fine for the cells, but a formula you wrote that points at the new data range is not rewritten for you, and the formulas you rewrite after the shift must use the final coordinates.
-
Shifting past the end is not an error; shifting rows over existing rows overwrites them silently.
-
Cost.
shiftRowstouches every cell in the range and every formula in the workbook that might refer to it; with tens of thousands of rows below the insertion point it is slow. Put variable-length blocks at the bottom of the sheet, or in their own sheet, and avoid shifting big ranges.
copyRows
The template can only contain so many example rows. When a block has to repeat (one block per loan, a subtotal line
after each author), copy a prototype block with XSSFSheet.copyRows. It is declared on XSSFSheet only, not on the
Sheet interface or on HSSFSheet, so cast (an .xls template has no copyRows). CellCopyPolicy says what is copied; its builder methods are cellValue,
cellStyle, cellFormula, copyHyperlink, mergeHyperlink, rowHeight, condenseRows and mergedRegions.
package com.bookshelf.office;
import org.apache.poi.ss.usermodel.CellCopyPolicy;
import org.apache.poi.xssf.usermodel.XSSFSheet;
public final class InsertCopies {
private InsertCopies() {
}
/** Inserts {@code count} copies of {@code prototypeRow} right under it, pushing the rest down. */
public static void insertCopies(XSSFSheet sheet, int prototypeRow, int count) {
int last = sheet.getLastRowNum();
if (count <= 0) {
return;
}
if (prototypeRow < last) {
sheet.shiftRows(prototypeRow + 1, last, count);
}
CellCopyPolicy policy = new CellCopyPolicy.Builder()
.cellValue(false) // keep only style (and formulas below)
.cellStyle(true)
.cellFormula(true)
.rowHeight(true)
.mergedRegions(false)
.build();
for (int i = 1; i <= count; i++) {
sheet.copyRows(prototypeRow, prototypeRow, prototypeRow + i, policy);
}
}
}
Source: XSSFSheet Javadoc (copyRows) and
CellCopyPolicy Javadoc.
On the Bookshelf template, insertCopies(sheet, 4, 3) took the last row from 7 to 10 and gave the three new rows the
prototype’s styles. copyRows copies rows inside one sheet; to copy between sheets you copy cell by cell (value, style
with cloneStyleFrom when the workbooks differ) or clone the whole sheet, as in the next section.
Cloning and removing sheets
When the output needs one sheet per branch, per member or per month, design one sheet in the template, clone it once for every group and delete the original.
| Call | Notes |
|---|---|
|
Deep copy of the sheet with an automatic name such as |
|
A sheet name has at most 31 characters, may not contain |
|
Removes the sheet and, per its Javadoc, makes another sheet active and selected when the removed one was. |
|
Hides a template sheet instead of deleting it; |
|
Reorder, and choose the tab that is shown when the file is opened. |
package com.bookshelf.office;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.poi.ss.usermodel.Name;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.SheetVisibility;
import org.apache.poi.ss.util.WorkbookUtil;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public final class BranchSheets {
private BranchSheets() {
}
/** One sheet per branch, cloned from the template sheet "Inventory", which is then removed. */
public static void write(InventoryTemplate template, Map<String, List<Book>> booksByBranch,
OutputStream out) throws IOException {
try (XSSFWorkbook wb = template.open()) {
int templateIndex = wb.getSheetIndex("Inventory");
for (Map.Entry<String, List<Book>> branch : booksByBranch.entrySet()) {
// sheet names: at most 31 characters, none of \ / ? * [ ] :
String name = WorkbookUtil.createSafeSheetName(branch.getKey());
Sheet copy = wb.cloneSheet(templateIndex);
wb.setSheetName(wb.getSheetIndex(copy), name);
// ... fill the copy here (cells, table rows) ...
}
// Names are NOT re-pointed or invalidated when a sheet disappears: delete the ones
// that still refer to the template sheet before removing it.
List<Name> stale = new ArrayList<>();
for (Name n : wb.getAllNames()) {
if ("Inventory".equals(n.getSheetName())
|| n.getRefersToFormula().startsWith("Inventory!")) {
stale.add(n);
}
}
stale.forEach(wb::removeName);
wb.removeSheetAt(templateIndex); // or: wb.setSheetVisibility(i, SheetVisibility.VERY_HIDDEN)
wb.setActiveSheet(0); // the active tab may now point past the end
wb.write(out);
}
}
}
Source: XSSFWorkbook.cloneSheet, Workbook Javadoc and WorkbookUtil Javadoc.
Observed in the test: after removeSheetAt a workbook-scoped name that pointed at the removed sheet was not
invalidated; it kept referring to Inventory!$B$1, a sheet that no longer existed, which Excel reports as an error on
open. That is why the example removes such names first. A formula in another sheet that referred to the removed sheet
has the same problem and must be rewritten.
Cloned sheets share the workbook’s style table, which is good: the styles are reused and the file does not grow with
the number of sheets. They also share the workbook-scoped names, which therefore point at one sheet only. When every
cloned sheet needs its own ReportTitle, create sheet-scoped names after cloning (createName(), setNameName,
setSheetIndex, setRefersToFormula) or address the cells by position instead of by name.
Formulas and cached values after filling
A formula cell in an .xlsx stores its text and a cached result. POI does not recalculate when you change an input,
so the cached values of the template’s formulas (the footer total, a share-of-total column) are the values from the
last time someone saved the file in Excel. Two safe choices:
-
Ask Excel to recalculate with
wb.setForceFormulaRecalculation(true), as the filler does. The file is flagged (fullCalcOnLoad="true"inworkbook.xml, which the test confirmed) and Excel and LibreOffice recompute everything when the file is opened. This is the right default for a download the user opens in a spreadsheet application. -
Evaluate in Java with a
FormulaEvaluatorwhen the cached value must be right without opening Excel (the file is read by another program, previewed in a browser, or imported by a tool that reads cached values). Callevaluator.evaluateAll()after you finish filling, understanding that not every Excel function is supported.
Both are described in Formulas & Evaluation. If you do neither, a viewer that does not recalculate shows the template’s old numbers next to the new data, which is the worst outcome because it looks plausible.
Charts and pivot tables built on the data are also cached: a chart keeps the cached points of the series it plots, and
a pivot table keeps its cache. POI does not refresh either. A chart that points at a fixed range ($A$5:$A$5) keeps
plotting that range after you add rows; point the chart at the table or at a dynamic defined name, or rebuild it with
the XDDF API (Images, Charts, Comments & Hyperlinks).
For a pivot table, the pivot cache definition can be marked to refresh when the file is opened; see
Tables, Named Ranges & Pivot Tables.
Macro-enabled templates (.xlsm)
An .xlsm file is an .xlsx package that also contains a xl/vbaProject.bin part (an OLE2 file holding the VBA
modules) and a different main content type. What POI does with it:
| Need | What POI supports |
|---|---|
Open an |
Yes. |
Turn a plain |
|
Read the macro source code |
Yes, |
Edit, run, sign or remove a macro |
No. POI never executes VBA. Removing the macros means writing the data into a macro-free workbook. |
Keep macros when the workbook goes through |
Not documented and not tested here; plan to keep the macro-enabled report on the XSSF path. |
The output file name must end in .xlsm, and the HTTP content type must be
application/vnd.ms-excel.sheet.macroEnabled.12; an .xlsm package saved under an .xlsx name makes Excel refuse to
open it.
package com.bookshelf.office;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import org.apache.poi.poifs.macros.VBAMacroReader;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public final class MacroTemplate {
public static void main(String[] args) throws IOException {
byte[] template = Files.readAllBytes(Path.of("books-inventory-macros.xlsm"));
// 1. Reading: list the VBA modules (POI only reads them, it cannot create or edit them)
try (VBAMacroReader reader = new VBAMacroReader(new ByteArrayInputStream(template))) {
Map<String, String> macros = reader.readMacros();
macros.forEach((module, code) ->
System.out.println(module + ": " + code.lines().count() + " lines of VBA"));
}
// 2. Filling: the vbaProject.bin part travels through the open/write cycle untouched.
// Opened from bytes, not from the File, so close() cannot write into the template.
try (InputStream in = new ByteArrayInputStream(template);
XSSFWorkbook wb = new XSSFWorkbook(in);
OutputStream out = Files.newOutputStream(Path.of("books-inventory-filled.xlsm"))) {
if (!wb.isMacroEnabled()) {
throw new IllegalStateException("Not a macro-enabled workbook");
}
wb.getSheetAt(0).getRow(0).createCell(1).setCellValue("Filled by POI");
wb.write(out); // the target file name MUST end in .xlsm
}
}
}
Source: VBAMacroReader Javadoc,
VBAMacroExtractor Javadoc and
XSSFWorkbook.isMacroEnabled.
This example compiles against POI 5.5.1; the macro-preservation claim above was checked with a stand-in part, and
VBAMacroReader was not run against a real VBA project in this test.
Macros are also the reason to be careful with uploads: a macro-enabled workbook sent by a user is executable content. Reading its data with POI is safe, because POI never runs it, but storing and re-serving the file is not. The upload pages of this section (a later group) cover the allow-list and the content checks.
Streaming on top of a template: SXSSFWorkbook(XSSFWorkbook)
A template can also start a streaming export. The designer provides the title, the header row, the styles and the
column widths; the code appends hundreds of thousands of rows through SXSSF without holding them in memory. The wrapper
is new SXSSFWorkbook(XSSFWorkbook template) or new SXSSFWorkbook(XSSFWorkbook, int rowAccessWindowSize).
The restriction is the one the constructor’s Javadoc states, and it was reproduced on a template whose rows 0-7 were filled:
| Operation after wrapping | Result in POI 5.5.1 |
|---|---|
|
|
|
|
|
|
|
Works. The output file contains the template rows followed by the new rows (verified by re-reading the file). |
Styles, fonts and formats of the template |
Reusable: read the |
Template tables, auto-filters, print areas and charts |
Kept as they are but not resized: a table in the template still ends where the template said. |
package com.bookshelf.office;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDate;
import java.util.Iterator;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public final class LoansReportFromTemplate {
public record Loan(long id, String member, String title, LocalDate loaned, LocalDate due) {
}
/**
* The template holds the title, the header row (index 2) and a prototype body row (index 3)
* that only carries styles. All of it is written as is; the loans start after it.
*/
public static void write(byte[] template, Iterator<Loan> loans, OutputStream out) throws IOException {
try (XSSFWorkbook xssf = new XSSFWorkbook(new ByteArrayInputStream(template))) {
XSSFSheet templateSheet = xssf.getSheetAt(0);
int firstFreeRow = templateSheet.getLastRowNum() + 1;
// Read the styles NOW: once wrapped, SXSSF cannot give the template rows back
CellStyle text = templateSheet.getRow(3).getCell(1).getCellStyle();
CellStyle date = templateSheet.getRow(3).getCell(3).getCellStyle();
// window of 200 rows; close() also deletes the temp files (POI 5.3+)
try (SXSSFWorkbook wb = new SXSSFWorkbook(xssf, 200)) {
Sheet sheet = wb.getSheetAt(0);
// sheet.getRow(0) == null here, and createRow(i) with i < firstFreeRow throws
int r = firstFreeRow;
while (loans.hasNext()) {
Loan loan = loans.next();
Row row = sheet.createRow(r++);
row.createCell(0).setCellValue(loan.id());
row.createCell(1).setCellValue(loan.member());
row.createCell(2).setCellValue(loan.title());
row.createCell(3).setCellValue(loan.loaned());
row.createCell(4).setCellValue(loan.due());
row.getCell(1).setCellStyle(text);
row.getCell(3).setCellStyle(date);
row.getCell(4).setCellStyle(date);
}
wb.write(out);
}
}
}
}
Source: SXSSFWorkbook Javadoc
(constructor SXSSFWorkbook(XSSFWorkbook)), POI How-To, SXSSF
and HybridStreaming example.
A run with 200,000 loans completed in about one second on a laptop, and re-reading the result showed the last row at
index 200003, the header row intact and the date format of the prototype row applied to the new date cells. The
streaming internals (window, flushing, temp files) are the subject of
Streaming Writes with SXSSF; close() also deletes the
temporary files, which since POI 5.3 is the call to make rather than dispose() (deprecated in 5.5.1).
When the design needs data above the template’s content (for example, rows inserted between the header and a footer), SXSSF cannot do it. Choose between a smaller XSSF report, a layout that puts the variable rows last, or building the fixed parts after the stream in a second pass.
Memory and performance
| Factor | Effect |
|---|---|
Template size |
An XSSF workbook is fully in memory, so a template with large embedded images costs that much for every concurrent
report. Keep images in the template small; compressing them in Excel is cheaper than any code change. Read the bytes
once and share the |
Loading time |
Parsing the template (unzipping, building the XML trees) is a fixed cost per report; measure it with your own template before introducing a cache of loaded workbooks, which you cannot share across threads anyway. |
Data volume |
XSSF holds every cell as objects. The usermodel path is comfortable up to tens of thousands of rows; beyond that use Streaming on top of a template: SXSSFWorkbook(XSSFWorkbook) or the pure streaming export of the SXSSF page. |
|
Linear in the number of cells below the insertion point, plus the formula adjustment. Prefer layouts where the variable part comes last. |
Styles |
Use the prototype row’s |
|
Copies the sheet XML in memory; fifty branch sheets from a template with a thousand rows means fifty thousand rows in memory. Fill them in a loop and write once, or stream. |
Pitfalls and security
| Pitfall | Why and what to do |
|---|---|
Overwritten template on disk |
|
A shared workbook across requests |
Workbooks are not thread safe. Share the bytes; open one workbook per report. |
Missing names after a template edit |
The designer renamed |
Table not resized or not even a table |
Writing rows below a table leaves it at the old size. Call |
Stale formulas and cached values |
Rewrite the ranges you changed and set |
Stale names and references after |
Remove or rewrite the names, formulas and validations that POI does not adjust. |
Data validation left behind |
|
User data in a template position |
Always |
Macro-enabled files from users |
Do not use a user-supplied |
Template as an attack surface |
If the business can upload templates, treat the upload as an untrusted file (size limits, zip-bomb limits, no macros), because it is parsed by the same code path as any other upload. |
Date and locale |
|
Wrong content type or extension on download |
|
What changed across versions
| Version | Relevant changes |
|---|---|
POI 3.x |
|
POI 4.0 to 4.1 |
SemVer starts (4.0). |
POI 5.2.3 |
The changes page lists "`XSSFWorkbook.cloneSheet` does not clone |
POI 5.3 |
|
POI 5.4 to 5.5 |
Stricter zip and XML handling for untrusted input, which applies to template uploads too. 5.5.0 added an API for reading
|
POI 6.0 (in development, not released) |
Per the POI site: JDK 17 or later, removal of the APIs deprecated in 5.x (the |
What POI does not provide here
-
No template engine. No
${...}expression language, no loops, no conditionals, no mail-merge. The filler is code you write, as above. Third-party projects exist for Excel (for example JXLS, built on POI) and for Word; the introduction page lists related projects. -
No automatic table growth.
setCellReferencesis your job, and so is the footer arithmetic. -
No full reference rewriting.
shiftRowsadjusts several kinds of reference but not tables or data validations, andremoveSheetAtleaves stale names. -
No macro creation, editing or execution. Read and preserve only.
-
No conversion of the result to PDF. A filled workbook stays a workbook; PDF generation needs another tool (see the PDF Generation section of this site).
-
No pivot table or chart refresh. Both keep cached data until a spreadsheet application recalculates them.
-
No
.xlsbwriting. The binary format is read only.
References
-
Spreadsheet Quick Guide (Files vs InputStreams, Named Ranges, Shift rows): https://poi.apache.org/components/spreadsheet/quick-guide.html
-
Spreadsheet How-To (SXSSF): https://poi.apache.org/components/spreadsheet/how-to.html
-
Spreadsheet limitations: https://poi.apache.org/components/spreadsheet/limitations.html
-
Javadoc,
XSSFWorkbook: https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/usermodel/XSSFWorkbook.html -
Javadoc,
XSSFTable: https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/usermodel/XSSFTable.html -
Javadoc,
XSSFSheet: https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/usermodel/XSSFSheet.html -
Javadoc,
Workbook: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/Workbook.html -
Javadoc,
CellCopyPolicy: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/CellCopyPolicy.html -
Javadoc,
SXSSFWorkbook: https://poi.apache.org/apidocs/dev/org/apache/poi/xssf/streaming/SXSSFWorkbook.html -
Javadoc,
VBAMacroReader: https://poi.apache.org/apidocs/dev/org/apache/poi/poifs/macros/VBAMacroReader.html -
Javadoc,
VBAMacroExtractor: https://poi.apache.org/apidocs/dev/org/apache/poi/poifs/macros/VBAMacroExtractor.html -
Official examples (
ShiftRows,CreateTable,HybridStreaming): https://github.com/apache/poi/tree/trunk/poi-examples/src/main/java/org/apache/poi/examples/ -
OpenXML4J (packages and parts): https://poi.apache.org/components/oxml4j/
-
Versioning policy: https://poi.apache.org/versioning.html
-
Release changes: https://poi.apache.org/changes.html
-
IANA media types: https://www.iana.org/assignments/media-types/media-types.xhtml