Pular para conteúdo

0020 — Leitura XLSX: Apache POI → FastExcel

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

Contexto

O alvo é deployar a aplicação como binário GraalVM native-image (RSS ~3× menor, imagem ~9× menor, build fora do host de produção — spec 012). O bloqueio era a leitura dos XLSX: ExcelSheetReader (motor SAX sobre XSSFReader/OPCPackage do Apache POI 5.3.0) + SheetSaxHandler liam via XMLBeans, cujo type-system por reflexão não compila em native-image (ClassCastException no StylesTable; sem fix no GraalVM CE). Enquanto POI estivesse alcançável no grafo de runtime, o native não fechava.

O projeto irmão bi-transporte-xls já havia trilhado o caminho (ADR central 0023 — Ler XLSX com FastExcel).

Decisão

  1. Reader por org.dhatim:fastexcel-reader:0.20.2 (StAX puro, zero XMLBeans). ExcelSheetReader foi reescrito preservando as 4 assinaturas públicas (readSheet, readDefaultSheet, readFirstRowOfSheet, listSheetNames) e a semântica de célula byte-a-byte: string → trim (blank→null); boolean → "true"/"false"; erro/vazio → null; numérico com formato de data → ddMMyyyy; numérico comum → getRawValue() (o texto <v> cru — não asNumber(), que introduziria .0/notação). A detecção de data POI-free (isDateFormat) reimplementa a lógica do DateUtil.isADateFormat (IDs built-in 14–22/45–47 + custom com token y/d/h/s).

  2. POI removido do projeto inteiro (nem implementation, nem testImplementation). As fixtures de teste passaram a ser escritas por XlsxFixtureWriter (FastExcel-writer, org.dhatim:fastexcel). Removido junto o bridge log4j-to-slf4j, que só existia porque o POI arrastava log4j-api.

  3. Paridade garantida por caracterização do reader (ReaderCaracterizacaoTest). O oráculo é o comportamento POI atual — não o antigo oráculo Python (exportar-json.py, projeto de Automação externo que não existe mais). Para cada um dos 9 tipos, a saída String[] do reader POI foi congelada como baseline (src/test/resources/caracterizacao/*.txt) e o reader FastExcel deve reproduzi-la idêntico. Um guard falha se algum TipoArquivo não tiver baseline.

Consequências

  • Native desbloqueado — nenhum XMLBeans no runtime; o rollout native é a Fase B da spec 012.
  • Cobertura declarada (REGRA Nº 3): os samples sintéticos são all-string, então a caracterização prova paridade de indexação de coluna / seleção de aba / trim / vazio→null nos 9 tipos. O ramo numérico-com-data (ddMMyyyy) não é alcançável por fixture sintética — o writer não preserva o formato-de-data (getDataFormatString null no roundtrip) — e fica coberto pelos golden-masters reais (RELATORIO18/META/TRR) + o teste unitário direto de isDateFormat.
  • Golden-masters Java×Python mantidos, não regenerados — o oráculo Python morreu; o resultado-python.json congelado ainda roda o ConformidadeTest, mas fixtures novas desse tipo não podem ser geradas.

Alternativas descartadas

  • Manter POI só em testImplementation (permitido pelo guard de CI, que só reprova POI não-test em app native): rejeitada por decisão do usuário — POI zero, alinhado ao piloto.
  • Escrever fixtures via FastExcel com data numérica estilizada para cobrir o ramo ddMMyyyy: inviável — o roundtrip writer→reader não preserva a detecção de formato-de-data (achado do piloto).