0025 — Views server-render com JTE¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-12 · Decidido em: 2026-08-25
Contexto¶
A decisão 0010 unificou o UI de admin/ops/dev da casa em
server-render com Thymeleaf (micronaut-views-thymeleaf), e o kit da casa
(0015/0017) nasceu nesse
motor. Depois, a 0022 tornou native-image o alvo de deploy
do server elegível.
Os dois não convivem bem. Micronaut Views usa Thymeleaf standalone → OGNL, e OGNL resolve todo
acesso a modelo por reflexão (Class.getDeclaredMethods + Method.invoke) — inclusive propriedade
(${m.status} → getStatus()) e método (${k.nome()}), e accessors aninhados (${v.mdfe().placa()}).
Sob AOT (closed-world), cada tipo alcançado por qualquer template precisa de @ReflectiveAccess;
@Introspected/@Serdeable/@MappedEntity não cobrem (a introspecção do Micronaut é reflection-free,
o OGNL não a usa). A falha aparece em runtime (500/TemplateProcessingException), não no build —
achada clicando tela por tela. O conjunto é ilimitado e drifta (view nova = tipo novo = mais um
rebuild). Custo real medido: o int-sascar exigiu 15 tipos anotados em 3 rebuilds native, mais
o MensagemM2m da própria lib xadm-mensageria (a view /admin/mensageria que ela fornece). É a
mesma classe "compila verde, quebra em runtime" que a §Native da engenharia já documentava.
Decisão¶
As views server-render da casa usam JTE (gg.jte via micronaut-views-jte), não Thymeleaf. JTE
compila cada .jte em uma classe Java no build; o princípio da 0010
(UI de admin/ops unificada, server-render) fica — muda o motor.
Por que JTE resolve a raiz:
- Type-check no build. O template declara
@paramtipados (@param ClienteForm form,@param List<SascarConta> contas);${k.nome()}vira chamada Java compilada. Campo/método errado = erro de compile, não 500 em runtime. É o gate compile-time que a casa prefere. - Native: reflexão fechada, não whack-a-mole. Precompilado, o JTE não reflete no modelo — ele
reflete só nas classes de template geradas (
gg.jte.generated.precompiled.Jte<Nome>Generated, instanciadas por nome) e lê os**/*.bin. É um conjunto estático, 1-por-.jte, num pacote fixo, capturado uma vez (tracing agent) e que não drifta — o oposto do "todo getter que o OGNL possa tocar". (Honesto: não é zero reflexão; é reflexão bounded e build-verificável.) - Imagem/boot menores (sem o motor de expressão dinâmico) e XSS escape content-aware por default
(
contentType = Html;$unsafe{}para cru).
Mecanismo (detalhe operável na engenharia): plugin gg.jte.gradle na mesma versão do runtime
gg.jte:jte (gerenciado pelo BOM via micronaut-views-jte); modo generate() (emite .java
compilado junto do app → binário native self-contained; micronaut.views.jte.dynamic=false em
prod/native); .jte em src/main/jte; @View("path") + model Map/POJO casa nos @param por
nome; layout via @template.kit.layout(title=…, content = @…) com @param gg.jte.Content content.
A casa fornece o reflect-config/resource-config das classes geradas (o micronaut-views-jte
não embarca esse hint) — receita na engenharia.
Correção 2026-09-03: o "não drifta" acima vale só com a extensão que auto-enumera as templates (
jteExtension('gg.jte.nativeimage.NativeResourcesExtension')+ depjteGenerate('gg.jte:jte-native-resources')), que gera oreflection-config.jsonde todas as*Generatedno build. A lista à mão drifta: view nova esquecida é classe podada no AOT, 404 só no native. A extensão é a norma e a lista à mão é proibida — detalhe em java-micronaut §JTE.Correção 2026-09-08: view empacotada em lib mora sob subpasta própria do
src/main/jte; na raiz, ela gera a mesma FQCN que a view do app, e uma sombreia a outra sem aviso. Norma em Bibliotecas da casa.
Alternativas descartadas¶
- Manter Thymeleaf +
@ReflectiveAccesspor tipo. É o whack-a-mole: ilimitado, drifta a cada view nova, falha em runtime, e a lista é achada clicando tela por tela (15 tipos / 3 rebuilds no piloto). Não se paga com native como default. - Thymeleaf + GraalVM
Featurepor pacote (registrar<app>.**por reflexão). É o "glob" real, mas exige a depsvm(coordenada incerta, não-cacheada), só valida em build native, over-registra o pacote inteiro, e não cura a falta de type-check nem o tamanho da imagem. - Thymeleaf + tracing agent. Automatiza a captura dos hints, mas branch
th:ifnunca renderizado no run do agente = hint não capturado → mesma classe de bug (falha em runtime numa tela pouco exercitada). Não elimina, só automatiza a caça. - Rocker (também compilado). Mesma categoria de ganho que o JTE, mas a manutenção desacelerou (poucos releases; risco de abandono) e o escape XSS é menos content-aware. A única vantagem clara do Rocker — perf near-zero-copy — é irrelevante para telas admin/CRUD (não-hot-path).
Consequências¶
- O kit da casa é re-feito em JTE (
layout.jte+headerRight.jte— o bloco direito, duplicado nos dois fragments Thymeleaf, vira um template chamado dos dois). Os templates rastreados de kit Thymeleaf (0015/0017) passam a apontar para o kit JTE; a versão Thymeleaf vira legado durante a migração. - Migração faseada da frota, app por app (Thymeleaf sai quando a última view do app é portada). É
mecânica:
th:each→@for,th:if→@if,${x.metodo()}→${x.metodo()}(agora compilado), fragment-replace→@template.layout(content=@…). As views são admin dev-edit (não designer), então perder "HTML abrível no browser" quase não pesa. - A
xadm-comum-web/xadm-mensageriaseguem o modelo (a view/admin/mensageriada mensageria vira.jte; os@ReflectiveAccessde view viram desnecessários e saem no review native). - Um hint native novo, mas fechado: o
reflect-configdas classesgg.jte.generated.precompiled.* resource-config**/*.bin, fornecido pela casa (template/receita), capturado uma vez.