0020 — GraalVM native-image (CE 25)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-08-20 · Decidido em: 2026-08-20
Contexto. O integrador roda como shadowJar na JVM em 6 deployments Coolify (um por cliente,
mesmo binário). Compilar para GraalVM native-image derruba RAM e boot por instância — economia
×6 — e move o build spike para off-host. O Micronaut já é nativo por design; os bloqueadores eram
duas libs sujas em native no grafo de alcance: Apache POI (export XLSX, arrasta XMLBeans +
java.awt.Font) e firebase-admin (push FCM, gRPC/reflexão).
Decisão.
- Export POI → FastExcel writer (
org.dhatim:fastexcel:0.20.2, StAX, sem XMLBeans/AWT).TableExportServicereescrito comWorkbook/Worksheet.value/finish. POI removido por completo — inclusive de teste: o teste de caracterização reabre o XLSX comorg.dhatim:fastexcel-reader(ReadableWorkbook), não POI. OruntimeOnly log4j-to-slf4jsaiu junto (existia só porque o POI trazia log4j-api). Padrão da casa (norma 0023, mesmo dobi-transporte-xls). - Largura de coluna heurística. FastExcel writer não tem
autoSizeColumn(o do POI usavajava.awt.Font, morto em native). Substituído porws.width(col, maxChars)commaxChars= maior contagem de char da coluna, teto 60. Mudança cosmética explícita (constituição §6/REGRA 3): as larguras não batem 1:1 com o autoSize antigo; o conteúdo tem paridade (provada no teste de caracterização). O autoSize antigo já era best-effort (try/catchignore). - Perfil de build native. Bloco
graalvmNative { binaries { named("main") } }:-PnativeQuick→-Ob(quick build, loop de diagnóstico) / sem a flag →-Os(otimiza tamanho, release);--gc=serialsempre (menor footprint de RAM). Reaproveita otoolchainDetection = falsee odockerfileNative.jdkVersion = "25"já presentes. - Reachability metadata só onde faltou. O
nativeCompilepassou de primeira, sem reflect-config manual: Micronaut + o GraalVM reachability-metadata repository cobrem firebase-admin (grpc-netty- shaded + protobuf entram na imagem) e o Sentry. O gap era de runtime, no boot: o logback instancia appenders por reflexão. reflect-config emsrc/main/resources/META-INF/native-image/br.com.xadm/integrador/registrandoConsoleAppender,RollingFileAppender,TimeBasedRollingPolicy,PatternLayoutEncoder,SentryAppender. E resource-config incluindofirebase-admin-sdk.json— carregado viagetResourceAsStreamem runtime, não auto-detectado; sem ele o push desabilitaria silenciosamente (default seguro: incluir mesmo que em prod a credencial venha de disco). - FCM: metadata bastou, sem rewrite. O
send()do FCM é HTTP v1 REST por baixo, não gRPC (o poço gRPC é do Firestore, que o app não usa). O fallback previsto — reescreverFcmPushServicepara FCM HTTP v1 cru — não foi acionado: compile inclui firebase-admin e o boot não quebra (init lazy).
Consequências / trade-offs.
- Imagem native
integrador:latest251MB (vsintegrador:preJVM 624MB). Boot limpo em native (/health→{"versao":"1.3.5","status":"UP"}), smoke do export verde (GET /export/xlsx/municipio→ 200, XLSX válido; tabela inexistente → 404). dockerBuildNativeexige--no-configuration-cache. Com o config-cache ligado (gradle.properties) a taskdockerfileNative/generateResourcesConfigFilefalha na serialização (bug Micronaut+Gradle). OnativeCompilenão encadeia essa task e passa com o cache. O build do Coolify deve passar a flag (ou desligar o config-cache) no caminho native.- Rollout incremental por instância (as 6 são o mesmo binário): subir 1 piloto — a de menor tráfego — validar, depois as demais. Rollback por instância.
- WARNINGs de grpc-netty-shaded no build (negotiators internos não encontrados) são benignos: os caminhos gRPC não são exercitados (FCM é HTTP v1).
Verificação. ./gradlew check verde (checkstyle + Testcontainers + piso JaCoCo). nativeCompile
-PnativeQuick verde; binário boota limpo e passa smoke dos caminhos quentes; dockerBuildNative
--no-configuration-cache gera a imagem. Envio FCM real em native não exercitado (dev tem
push.enabled=false; exige credencial + FCM de prod) — fica para o smoke da instância piloto.
Fora de escopo: rewrite do FcmPushService para HTTP v1 (fallback não acionado); app central
notificacao.xadm.biz (centralizaria email/push/telegram/wa; longo prazo); o e2e funcional (vive e
roda em outro repo). Ver o runbook de build em dev/native-image.