Spreadsheet Usermodel Basics
|
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; the 6.0 line
is in development and has not been released), Java 21 and later, Spring Boot 4.x on Spring Framework 7 and Jakarta EE 11.
It is the first hands-on page about Excel: it explains the object model that every other spreadsheet page builds on, the
four interfaces Workbook, Sheet, Row and Cell, how to create, open, read, write and close a workbook, and the
small helper classes for cell addresses and ranges. The examples use the Bookshelf scenario described in the
section landing page. The big picture (components, jars, the three API levels) is in
Introduction & Architecture, the Maven and Gradle
coordinates in Setup & Dependencies, and the two physical
formats in File Formats: OOXML & OLE2.
The model: workbook, sheets, rows and cells
An Excel file is a workbook that contains an ordered list of sheets. A sheet is a sparse grid of rows, and a row is
a sparse list of cells. POI mirrors this one to one in the package org.apache.poi.ss.usermodel ("ss" is for
spreadsheet), which lives in the poi jar and contains interfaces only for the core types:
| Interface | Represents | Main operations |
|---|---|---|
|
The whole file. Owns the sheets, the cell styles, the fonts, the data formats, the defined names and the shared string table. |
|
|
One worksheet (a tab). Also holds sheet-wide settings such as column widths, merged regions, freeze panes and the auto-filter. |
|
|
One row of a sheet, identified by a zero-based index. |
|
|
One cell: a value (or a formula), a type, and a reference to a |
|
|
Factory for objects that belong to the workbook but are not created by it directly. |
|
|
Formatting objects, shared between cells. Covered in Cell Styles, Fonts & Formats. |
|
Every interface has one implementation per file format, and a third one for streaming writes. Code that is written against the interfaces works for all of them; code that needs a format-specific feature casts or uses the concrete class.
How the interfaces map to the files:
| Interface | XSSF (.xlsx, OOXML) |
HSSF (.xls, OLE2) |
|---|---|---|
|
The package part |
The |
|
One part per sheet, |
The records between a |
|
A |
A |
|
A |
One record per cell: |
The model is sparse and zero-based. A row or a cell that was never written does not exist: Sheet.getRow(7)
returns null when nothing was written on the eighth row, and Row.getCell(3) returns null for an absent cell. Row and
column indices start at 0, so the cell that Excel shows as C2 is row 1, column 2.
Choosing the implementation
Which concrete class you get depends on the format and on how you obtain the workbook.
| Class | Jar | Format | Use it for |
|---|---|---|---|
|
|
|
Creating and editing workbooks in memory. The default choice for new files. |
|
|
|
Very large exports with a constant memory footprint. See Streaming Writes with SXSSF. |
|
|
|
Legacy files only. See Legacy .xls with HSSF. |
|
|
Either |
Opening an existing file when you do not know (or do not care) which format it is. |
WorkbookFactory is a static factory, not a workbook. It inspects the first bytes of the input (with FileMagic) and
returns an XSSFWorkbook for an OOXML package or an HSSFWorkbook for an OLE2 file. It is declared in poi but the
.xlsx branch needs poi-ooxml on the classpath at run time; without it, opening an .xlsx fails with a
ClassNotFoundException-style error rather than a clear message, which is one of the problems the setup page addresses.
Creating a workbook directly
Workbook xlsx = new XSSFWorkbook(); // empty .xlsx workbook, no sheets yet
Workbook xls = new HSSFWorkbook(); // empty .xls workbook (legacy)
Source: POI Quick Guide, New Workbook
An empty workbook has no sheet; most operations fail until you call createSheet. Writing a workbook with no sheet
produces a file that Excel refuses to open as valid, so always create at least one.
Opening an existing workbook
WorkbookFactory.create is overloaded; the useful forms are:
| Signature | Behaviour |
|---|---|
|
Opens the file read-write; detects the format; no password. |
|
As above; the password decrypts an encrypted file. An encrypted file opened without a password (or with a wrong one)
throws |
|
|
|
Reads the whole stream. Needed for uploads that arrive as a stream, but costs more memory than a |
|
Creates a new, empty workbook: an |
The difference between File and InputStream matters for .xlsx:
-
With a
File, OpenXML4J opens the ZIP file as a random-access archive instead of buffering it.XSSFWorkbookstill parses every sheet when it opens, so for large or many-sheet files useXSSFReaderwith SAX instead. -
With an
InputStream, the ZIP cannot be accessed randomly, so POI first copies the entire stream into memory (or, for very large inputs, into a temporary file, depending on the configuration) before it can read any part. The input stream is not the cheaper option even for a small file; it is simply the only one that exists for data that is not on disk. -
With a read-write
File, closing the workbook can write the changes back to the original file. If you only read, passreadOnly = trueand the file can never be modified by accident. This is the rule for every import in this section.
In a web application, store the uploaded multipart file to a temporary file first and open it as a File, as shown
in the import pages of this section. It is faster and uses less heap than keeping a copy of the upload in memory.
Creating and writing a workbook
The example creates the first sheet of books-inventory.xlsx from the Bookshelf scenario with text, numbers, a date, a
boolean, a rich text value and a formula. It stays on the basics: styles, formats, validation and charts come in the
following pages.
package bookshelf;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.util.WorkbookUtil;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
public class WriteInventory {
record Book(String isbn, String title, int copies, double price, LocalDate acquired, boolean available) { }
public static void main(String[] args) throws IOException {
List<Book> books = List.of(
new Book("978-0-13-468599-1", "Effective Java", 4, 45.90, LocalDate.of(2024, 3, 14), true),
new Book("978-1-61729-994-3", "Spring in Action", 2, 49.99, LocalDate.of(2025, 1, 9), false));
Path target = Path.of("books-inventory.xlsx");
try (Workbook workbook = new XSSFWorkbook();
OutputStream out = Files.newOutputStream(target)) {
// Sheet names may not contain : \ / ? * [ ] and are limited to 31 characters.
String sheetName = WorkbookUtil.createSafeSheetName("Books [2026/Q4]: inventory");
Sheet sheet = workbook.createSheet(sheetName);
CreationHelper helper = workbook.getCreationHelper();
Row header = sheet.createRow(0);
String[] titles = {"ISBN", "Title", "Copies", "Price", "Acquired", "Available"};
for (int c = 0; c < titles.length; c++) {
header.createCell(c, CellType.STRING).setCellValue(titles[c]);
}
int rowIndex = 1;
for (Book book : books) {
Row row = sheet.createRow(rowIndex++);
row.createCell(0).setCellValue(book.isbn());
row.createCell(1).setCellValue(helper.createRichTextString(book.title()));
row.createCell(2).setCellValue(book.copies());
row.createCell(3).setCellValue(book.price());
row.createCell(4).setCellValue(book.acquired()); // stored as a number, needs a date style to display
row.createCell(5).setCellValue(book.available());
}
Row total = sheet.createRow(rowIndex);
total.createCell(0).setCellValue("Total copies");
total.createCell(2).setCellFormula("SUM(C2:C" + rowIndex + ")");
workbook.write(out);
}
System.out.println("Written " + target.toAbsolutePath());
}
}
Source: POI Quick Guide, New Workbook and POI Quick Guide, Creating Cells. The example was compiled and run against POI 5.5.1 with Java 21.
What the example shows, step by step:
-
try (Workbook workbook = ...)closes the workbook at the end.WorkbookextendsCloseable; closing releases the underlying package, the temporary files POI may have created and the file handle of a workbook opened from a file. Wrap theOutputStreamin the sametryso that it is closed whatever happens. -
WorkbookUtil.createSafeSheetNameturns any string into a legal sheet name (see the next section). -
createRow(0)andcreateCell(c, CellType.STRING)create the row and the cell.createCell(c)without a type creates a blank cell whose type is set by the firstsetCellValue. -
The overloads of
setCellValuepick the cell type:StringandRichTextStringgive a string cell,double(and theintthat widens to it) a numeric cell,booleana boolean cell, andDate,Calendar,LocalDateandLocalDateTimea numeric cell that holds the Excel serial date. -
setCellFormulatakes the formula text without the leading=. POI stores the text; it does not calculate a result. See Formula cells below. -
workbook.write(out)serialises the whole workbook. Do not depend onwriteclosing or not closing the stream.
Sheet names
A sheet name is validated when the sheet is created. createSheet throws IllegalArgumentException for an invalid name
and for a duplicate: the comparison is case-insensitive, so creating "a" after "A" fails with
The workbook already contains a sheet named 'a'.
| Rule | Detail |
|---|---|
Length |
1 to 31 characters |
Forbidden characters |
|
Apostrophe |
A name may not start or end with a single quote |
Reserved name |
Excel reserves |
Uniqueness |
Case-insensitive within the workbook |
WorkbookUtil.createSafeSheetName(String) replaces every forbidden character with a blank and truncates the result to
31 characters, so "Books [2026/Q4]: inventory and a very long tail" becomes "Books 2026 Q4 inventory and ".
It does not guarantee uniqueness, and it does not trim: when sheet names come from user data (a library name, a
report parameter), call it, then make the result unique yourself, for example by appending a counter inside the 31
characters. WorkbookUtil.validateSheetName(String) throws instead of repairing, which is useful for input
validation. Never put user-controlled text into a sheet name unchecked: it is also used in formulas and in the names of
package parts.
Formula cells
setCellFormula stores the formula only. A workbook created by POI has no cached result in the formula cells, which has
two consequences:
-
Excel, LibreOffice and other spreadsheet programs recalculate on open, so people see the values. To force this explicitly, call
workbook.setForceFormulaRecalculation(true)(orsheet.setForceFormulaRecalculation(true)for one sheet). -
A program that reads the file without calculating, for example a script, a preview pane or POI itself, sees an empty cached value. In the reading example below, the total cell is a formula cell whose cached numeric result is
0.0. If you need the value on the server, evaluate the formulas with theFormulaEvaluator(see Formulas & Evaluation).
Opening, reading and closing
The next example opens the file written above, reads it with getRow and getCell, and uses a MissingCellPolicy to
decide what a hole in the data means.
package bookshelf;
import java.io.File;
import java.io.IOException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Row.MissingCellPolicy;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
public class ReadInventory {
public static void main(String[] args) throws IOException {
File file = new File("books-inventory.xlsx");
// password null: unencrypted; readOnly true: the file is never written back.
try (Workbook workbook = WorkbookFactory.create(file, null, true)) {
System.out.println("Format: " + workbook.getClass().getSimpleName()
+ ", sheets: " + workbook.getNumberOfSheets());
Sheet sheet = workbook.getSheetAt(0);
System.out.println("Rows " + sheet.getFirstRowNum() + ".." + sheet.getLastRowNum());
for (int r = sheet.getFirstRowNum() + 1; r <= sheet.getLastRowNum(); r++) {
Row row = sheet.getRow(r); // null if the row was never written
if (row == null) {
continue;
}
// CREATE_NULL_AS_BLANK never returns null (but it adds a blank cell to the sheet!)
// RETURN_BLANK_AS_NULL treats blank cells and missing cells the same way.
Cell isbn = row.getCell(0, MissingCellPolicy.RETURN_BLANK_AS_NULL);
Cell copies = row.getCell(2, MissingCellPolicy.RETURN_BLANK_AS_NULL);
Cell acquired = row.getCell(4, MissingCellPolicy.RETURN_BLANK_AS_NULL);
System.out.printf("row %d: isbn=%s copies=%s acquired=%s%n", r + 1,
isbn == null ? "-" : isbn.getStringCellValue(),
copies == null ? "-" : describe(copies),
acquired == null ? "-" : describe(acquired));
}
}
}
static String describe(Cell cell) {
// For a formula cell, look at the type of the cached result.
CellType type = cell.getCellType() == CellType.FORMULA ? cell.getCachedFormulaResultType() : cell.getCellType();
return switch (type) {
case STRING -> cell.getStringCellValue();
case NUMERIC -> DateUtil.isCellDateFormatted(cell)
? cell.getLocalDateTimeCellValue().toLocalDate().toString()
: Double.toString(cell.getNumericCellValue());
case BOOLEAN -> Boolean.toString(cell.getBooleanCellValue());
case ERROR -> "#ERR" + cell.getErrorCellValue();
case BLANK, _NONE, FORMULA -> "";
};
}
}
Output for the file written above:
Format: XSSFWorkbook, sheets: 1
Rows 0..3
row 2: isbn=978-0-13-468599-1 copies=4.0 acquired=45365.0
row 3: isbn=978-1-61729-994-3 copies=2.0 acquired=45666.0
row 4: isbn=Total copies copies=0.0 acquired=-
Source: POI Quick Guide, Cell contents and the Javadoc of WorkbookFactory and Row.MissingCellPolicy.
Two things in the output are deliberate lessons:
-
acquired=45365.0: the date was written as a plain number because the cell has no date style, soDateUtil.isCellDateFormattedisfalse. Excel shows45365too. A date is a number plus a date format; the format is part of the cell style, which is the subject of Dates, Numbers & DataFormatter. -
copies=0.0on the totals row: the cell is a formula that POI never evaluated, so the cached result is the default0.
Cell types
Cell.getCellType() returns the CellType enum. The old integer constants (HSSFCell.CELL_TYPE_STRING …) were
deprecated in 3.15 and removed in POI 4.0; the transitional getCellTypeEnum() was deprecated in 4.0 and removed in 5.0.
Do not use code that still has them.
CellType |
Meaning | Getter that is valid |
|---|---|---|
|
A number. Also every date and time, which Excel stores as a number of days since 1900. |
|
|
Text, up to 32,767 characters. |
|
|
|
|
|
A formula. The result type is a second, separate type. |
|
|
An existing cell with no value (it may have a style). |
none |
|
An error value such as |
|
|
Internal marker for "no type"; it never occurs in a cell read from a file. The leading underscore is intentional. |
none |
Calling the wrong getter is a classic server bug. getStringCellValue() on a numeric cell throws
IllegalStateException: Cannot get a STRING value from a NUMERIC cell, and getNumericCellValue() on a text cell
throws too. The only forgiving combination is a blank cell, which returns an empty string. User-supplied workbooks
mix types in a column constantly (an ISBN typed as a number, a price typed as text), so an import must switch on the cell
type, or use DataFormatter to read everything as the text the user sees. Both strategies are in
Dates, Numbers & DataFormatter.
How to change a cell’s type:
-
Call
setCellValuewith a value of the new kind. The type follows the value. -
Call
setBlank()to empty a cell. This is the replacement for the deprecatedsetCellType(CellType.BLANK). -
Cell.setCellType(CellType)is deprecated since POI 4.x (it converted the content in surprising ways); it is expected to disappear in the 6.0 clean-up. Choose the type when you create the cell (createCell(index, type)) and letsetCellValuedo the rest. -
getCellType()of a formula cell isFORMULA, not the type of its result. UsegetCachedFormulaResultType(), asdescribedoes above.
Missing cells and MissingCellPolicy
There are three states for a position in a row: the cell does not exist, the cell exists and is BLANK, or it has a value.
Spreadsheets produced by people have all three in the same column. The MissingCellPolicy argument of
Row.getCell(int, MissingCellPolicy) tells POI how to report the first two:
| Policy | Cell does not exist | Cell is BLANK |
Notes |
|---|---|---|---|
|
|
The blank cell |
Keeps the distinction. A blank cell may still carry a style. |
|
|
|
The simplest for imports: "no value" is always |
|
A new blank cell is added to the row and returned |
The blank cell |
Never returns |
In an import, use RETURN_BLANK_AS_NULL and map null to "no value". Use CREATE_NULL_AS_BLANK only when you are
about to write into the cell anyway (for example, to apply a style to a column of a template).
Rows and the length of a sheet
| Method | Behaviour and trap |
|---|---|
|
Zero-based indices of the first and last row that exists. Both return |
|
The number of rows that exist (not the index of the last one). A sheet with rows 0 and 1000 has 2. |
|
Zero-based index of the first cell, or |
|
The index of the last cell plus one (so it equals the "width" of the row), or |
|
The number of cells that exist. |
|
If a row already exists at that index, it is replaced by a new empty row, and its cells are lost. Fetch it first with
|
|
The same for a cell: an existing cell at that index is replaced. |
The "create only if absent" idiom, repeated everywhere in report code:
Row row = Objects.requireNonNullElseGet(sheet.getRow(index), () -> sheet.createRow(index));
Cell cell = Objects.requireNonNullElseGet(row.getCell(column), () -> row.createCell(column));
Source: Sheet Javadoc and Row Javadoc.
Iterating and addressing
There are two ways to walk a sheet. Sheet implements Iterable<Row> and Row implements Iterable<Cell>: the
iteration visits only rows and cells that exist, in ascending order, and silently skips the gaps. The index loop of the
reading example visits every position, so gaps surface as null. Choose by what a gap means:
| Style | Good for | Watch out |
|---|---|---|
Iterators ( |
Dumping all content, searching, transforming whatever is there |
The cell’s column is lost unless you read |
Index loops with |
Tabular data with known columns (the Bookshelf import: ISBN is always column 0) |
You must handle |
The second example iterates over the whole workbook and shows the three address helpers of
org.apache.poi.ss.util.
package bookshelf;
import java.io.File;
import java.io.IOException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.util.CellAddress;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
public class Iterate {
public static void main(String[] args) throws IOException {
try (Workbook workbook = WorkbookFactory.create(new File("books-inventory.xlsx"))) {
Sheet sheet = workbook.getSheetAt(0);
// Iteration visits only rows and cells that exist, in ascending order; gaps are skipped.
for (Row row : sheet) {
for (Cell cell : row) {
CellAddress address = cell.getAddress(); // e.g. C2
System.out.println(address.formatAsString() + " -> " + cell.getCellType());
}
}
CellReference ref = new CellReference("Sheet1!$D$3");
System.out.println(ref.getRow() + "," + ref.getCol() + " absolute row=" + ref.isRowAbsolute());
System.out.println(CellReference.convertNumToColString(27)); // AB
System.out.println(CellReference.convertColStringToIndex("AB")); // 27
CellRangeAddress range = CellRangeAddress.valueOf("A1:F3");
System.out.println(range.getNumberOfCells() + " cells, " + range.formatAsString());
System.out.println(range.isInRange(2, 5)); // row 2, col 5
}
}
}
Output (shortened):
A1 -> STRING
B1 -> STRING
...
C4 -> FORMULA
2,3 absolute row=true
AB
27
18 cells, A1:F3
true
Source: POI Quick Guide, Iterate over rows and cells and the Javadoc of CellReference, CellAddress and CellRangeAddress.
The three address classes
| Class | What it models | Typical use |
|---|---|---|
|
A row and a column. Parses and formats |
Error reports ( |
|
A cell reference as it appears in a formula: optional sheet name, absolute markers ( |
Building and parsing formula text, converting |
|
A rectangular range |
Merged regions, auto-filters, data validation ranges, print areas, named ranges |
Remember that all of them are zero-based internally (CellAddress("C2") is row 1, column 2) while the text form is
one-based, as in Excel. When you build an error message for a user, add 1 to the row index, or use formatAsString(),
which already does.
Limits of the format
The limits belong to the file format, not to POI, but POI enforces them and throws when you exceed them. They are exposed
by the SpreadsheetVersion enum, so code can adapt to the format of the workbook:
Limit (SpreadsheetVersion getter) |
EXCEL97 (.xls) |
EXCEL2007 (.xlsx) |
|---|---|---|
Rows ( |
65,536 |
1,048,576 |
Columns ( |
256 |
16,384 |
Characters in one cell ( |
32,767 |
32,767 |
Cell styles in the workbook ( |
4,000 |
64,000 |
Sheet name length |
31 |
31 |
How to apply this on the server:
SpreadsheetVersion version = workbook instanceof HSSFWorkbook
? SpreadsheetVersion.EXCEL97
: SpreadsheetVersion.EXCEL2007;
if (rowIndex >= version.getMaxRows()) {
throw new IllegalStateException("The export has more rows than this format supports");
}
Source: SpreadsheetVersion Javadoc and POI Spreadsheet Limitations.
A few more practical limits of the usermodel, from the POI documentation and from experience:
-
Text longer than 32,767 characters is rejected (
IllegalArgumentExceptionfromsetCellValue). Truncate or split long descriptions before writing them. -
The number of cell styles is capped (see the table). Creating one style per cell exhausts it quickly; create styles once and reuse them, as explained in Cell Styles, Fonts & Formats.
-
Numbers are stored as IEEE 754 doubles, like in Excel. An integer above 253 (a 19-digit identifier) or a
BigDecimalwith many digits loses precision. Write identifiers such as ISBNs and phone numbers as strings, which also keeps their leading zeros. -
Excel displays at most 15 significant digits of a number whatever the stored value is.
What it costs: memory and performance
The usermodel is convenient and expensive. Everything is kept in memory as a tree of objects:
| Aspect | Behaviour |
|---|---|
Heap |
For |
Reading |
|
Writing |
|
Shared strings |
Text cells are stored once in the workbook’s string table. Writing thousands of identical strings (status names, ISBN prefixes) is cheap; thousands of unique long strings are not. |
|
Needs font metrics (AWT) and measures every cell of the column: slow for big sheets, and it can fail on a headless
server without fonts installed. Call it once per column, after the data is written, or set fixed widths with
|
Rules of thumb for the usermodel: it is a good choice up to some tens of thousands of rows, it is the only API that supports random access, styles, formulas, charts and templates together, and it is a poor choice for a million-row export or for reading a huge upload. For those, the two streaming pages replace it: Streaming Writes with SXSSF and Streaming Reads: Event & SAX APIs.
Pitfalls and security
| Pitfall | What to do |
|---|---|
One workbook, many threads |
|
Sharing styles across workbooks |
A |
Not closing the workbook |
Leaks a file handle and, on SXSSF, temporary files. Always use try-with-resources. |
Opening read-write by accident |
|
|
The cell is absent. Use a |
|
The getter does not match the cell type. Switch on |
Date shown as a number |
The cell has no date format. Set a |
Formulas without values |
POI does not calculate on write. Use |
Leading zeros lost |
The value was numeric. Write identifiers as strings. |
Untrusted workbook |
A user-supplied |
Formula injection in exports |
A text value that starts with |
What changed across versions
| Version | Change relevant to the usermodel basics |
|---|---|
3.x |
Integer cell types ( |
4.0 |
The integer constants (deprecated in 3.15) were removed in favour of |
4.x and 5.x |
|
5.5.x |
The latest line on 2026-10-09 (5.5.1): needs Java 8 or later and receives critical and security fixes. |
6.0 (planned, not released) |
The POI site announces JDK 17 as the minimum, the removal of APIs deprecated in the 5.x line (so
|
What POI does not provide here
-
No calculation of formula results when writing. The evaluator exists, but it is a separate step, and it supports most but not all Excel functions.
-
No automatic type conversion. POI never turns the text
"12"into a number or a date for you. -
No object mapping. There is no
@Columnannotation or bean binding in POI. Mapping a row to a Java record, as the Bookshelf import does, is code you write (or a library such as the community Spring Batch Excel reader, covered later in the section). -
No rendering. Column widths, row heights and styles are stored, but POI never lays out the sheet or converts it to PDF. Converting a workbook to PDF needs another tool.
-
No Excel user interface. VBA macros are neither run nor created, and there are no macro-enabled outputs from the usermodel.
References
-
Spreadsheet components: https://poi.apache.org/components/spreadsheet/
-
Spreadsheet Quick Guide: https://poi.apache.org/components/spreadsheet/quick-guide.html
-
Spreadsheet How-To: https://poi.apache.org/components/spreadsheet/how-to.html
-
Spreadsheet limitations: https://poi.apache.org/components/spreadsheet/limitations.html
-
Javadoc,
org.apache.poi.ss.usermodel: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/usermodel/package-summary.html -
Javadoc,
org.apache.poi.ss.util: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/util/package-summary.html -
Javadoc,
SpreadsheetVersion: https://poi.apache.org/apidocs/dev/org/apache/poi/ss/SpreadsheetVersion.html -
Official examples: https://github.com/apache/poi/tree/trunk/poi-examples/src/main/java/org/apache/poi/examples/
-
Versioning policy: https://poi.apache.org/versioning.html
-
Release changes: https://poi.apache.org/changes.html