Pular para conteúdo

0023 — Ler XLSX com FastExcel-reader, não Apache POI

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-12 · Decidido em: 2026-08-19

Contexto

Vários apps da casa leem planilhas XLSX (processadores de dados: BI, integração, importadores). A biblioteca usada por padrão em Java para isso é Apache POI (poi-ooxml). Com a adoção de GraalVM native-image como alvo de deploy (0022), POI virou o único bloqueio técnico real da migração.

POI lê XLSX (formato XSSF) via XMLBeans, cujo type-system é carregado por reflexão + recursos binários e classes geradas org.openxmlformats.schemas.**. Em native-image (mundo fechado, sem descoberta em runtime) o XMLBeans não resolve o type-system e cai no tipo genérico → estoura um ClassCastException no StylesTable (XmlComplexContentImpl cannot be cast to StyleSheetDocument). Não há caminho estável na Community Edition: não existe reachability-metadata oficial para POI/XMLBeans, a configuração manual via tracing-agent é frágil (reabre a cada bump de POI), e a issue upstream que descreve exatamente este caso — oracle/graal#1993 — foi fechada sem solução consolidada.

Decisão

Para LER planilhas XLSX, use org.dhatim:fastexcel-reader. NÃO use Apache POI no main.

  • FastExcel-reader é native-safe por construção. Ele não carrega XMLBeans nem type-system por reflexão; parseia o XLSX com um StAX pull-parser (a implementação Aalto, com.fasterxml:aalto-xml, entra como dependência de compilação) — dependências comuns, amigáveis ao native-image. É por isso que compila e roda em native sem hack nenhum, não porque use "o StAX do JDK".
  • POI só em testImplementation. Gerar .xlsx de fixture no teste é uso legítimo de POI — fica fora do main, não entra no binário native. implementation/api de poi-ooxml num app native é proibido (guarda no ci.yml da casa).
  • Exceção declarada (§1.6 / REGRA Nº 3). FastExcel não cobre estilo rico (cores/fontes/bordas) nem avaliação de fórmula (devolve o valor cacheado). App que depende disso pode usar POI — mas esse app não vai a native (fica JVM, com limite de memória no Coolify). É uma decisão declarada por app, não silêncio: registre o porquê no repo do app.
  • Escrever XLSX: preferir org.dhatim:fastexcel (o writer do mesmo projeto, também sem XMLBeans) antes de recorrer a POI. Esta RD normatiza a leitura; o writer é a preferência análoga.

Correção 2026-09-12: a guarda de POI é do job gate do pipeline.yml (0027) e sabe que o app é native quando o build.targets do docs/app.json contém native.

Como se lê (o essencial)

Detalhe operacional (tipo neutro, detecção de data, streaming) na receita de engenharia/java-micronaut §Específico. Em resumo: a API de célula do FastExcel (asDate() → LocalDateTime, getRawValue(), getDataFormatId(), getDataFormatString()) cobre o uso típico de leitura; datas se detectam pelos IDs de formato built-in (14–22 / 45–47) ou pelo token de data no format string, já que o FastExcel não expõe um helper isDate. A leitura é streaming (Sheet.openStream() → Stream<Row>), preservando o baixo custo de memória.

Alternativas descartadas

  • POI + reachability-metadata manual (tracing-agent). Frágil e não-consolidado: a configuração reabre a cada bump de POI, não há metadata oficial mantida, e as issues upstream do próprio Graal sobre exatamente este caso foram fechadas sem fix. Custo permanente por um problema que o FastExcel não tem.
  • Manter POI e não ir a native. Renuncia ao ganho estratégico (RSS/imagem menores, sem build-spike no host — 0022) por uma biblioteca de leitura substituível. Só se justifica na exceção de estilo-rico/fórmula.

Consequências

  • Migrar de POI para FastExcel é troca de motor, não de contrato. Isole a leitura numa camada que produza um tipo neutro (ex. String[] por linha) preservando a semântica célula-a-célula; os mapeadores de domínio a jusante não mudam.
  • Paridade é obrigatória antes do native. Um golden-master (fixture real + resultado esperado) tem de passar antes de compilar native — é o gate que garante que trocar o motor não mudou o dado (constituição §6). Ver e2e native.
  • O ci.yml da casa reprova POI em main de app native. Guarda estática que pega o bloqueio antes do runtime (o build compila; quebra em runtime). Ver java-micronaut §Armadilhas.