Cria, revisa e padroniza listeners de persistência Sankhya com `@Listener` (classe estende `PersistenceEventAdapter`) — reage a eventos CRUD (before/after insert, update, delete) de qualquer entidade JAPE, inclusive instâncias nativas. Use ao criar, alterar, revisar, auditar ou padronizar reação a gravação/exclusão de registros, ao validar ou preencher campos automaticamente no insert/update, ao implementar auditoria de alterações, ao reagir a mudança de status de um registro, ao bloquear operações com exceção em `before*`, ao trabalhar com classes `*Listener` de persistência, ou ao tocar em código com `@Listener`/`PersistenceEventAdapter`/`PersistenceEvent`. NÃO usar para interceptar busca/carregamento/leitura de entidades (`FinderListener`, filtro em query) — isso é `@BeforeLoadListener`, skill `before-load-listener`.
Installation
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Cria, revisa e padroniza listeners de persistência Sankhya com `@Listener` (classe estende `PersistenceEventAdapter`) — reage a eventos CRUD (before/after insert, update, delete) de qualquer entidade JAPE, inclusive instâncias nativas. Use ao criar, alterar, revisar, auditar ou padronizar reação a gravação/exclusão de registros, ao validar ou preencher campos automaticamente no insert/update, ao implementar auditoria de alterações, ao reagir a mudança de status de um registro, ao bloquear operações com exceção em `before*`, ao trabalhar com classes `*Listener` de persistência, ou ao tocar em código com `@Listener`/`PersistenceEventAdapter`/`PersistenceEvent`. NÃO usar para interceptar busca/carregamento/leitura de entidades (`FinderListener`, filtro em query) — isso é `@BeforeLoadListener`, skill `before-load-listener`.
Listener de Persistência (@Listener) — Addon Studio 2.0
@Listener executa lógica personalizada em eventos de persistência (CRUD) de uma entidade JAPE — antes/depois de insert, update e delete. Funciona em entidades do próprio add-on e em instâncias nativas do sistema (CabecalhoNota, Parceiro, Produto, etc.). A anotação registra o listener automaticamente — sem edição manual de XML.
⚠️ @Listener ≠ @BeforeLoadListener. São mecanismos distintos, com anotação, interface, evento e escopo diferentes: @Listener reage a escrita (insert/update/delete, via PersistenceEventAdapter); @BeforeLoadListener intercepta leitura (busca/carregamento no Finder, via FinderListener) — e só funciona em instâncias do próprio add-on. Tarefa de leitura/filtro de busca → skill before-load-listener.
1. Quando usar — @Listener vs @BeforeLoadListener vs @BusinessRule vs @ActionButton
Mecanismo
Escopo
Quando usar
@Listener
CRUD (insert/update/delete) de qualquer entidade
Validar/preencher campos ao gravar/excluir; auditoria; reagir a mudança de dados.
@BeforeLoadListener
Toda busca/carregamento de UMA entidade (só do add-on)
Filtro transversal em leitura (segurança, multi-tenant, soft-delete).
@BusinessRule
Confirmação/faturamento de notas
Regras transacionais comerciais com barramento de regras.
@ActionButton
Ação disparada pelo usuário na tela
Processamento sob demanda, não automático por evento.
Regra rápida: reagir a gravação/exclusão de registro (automático, sem clique)? @Listener. Filtrar leitura? @BeforeLoadListener. Regra de nota no barramento comercial? @BusinessRule.
Diferente do @BeforeLoadListener, o @Listenerpode escutar instâncias nativas do sistema (CabecalhoNota, Financeiro, Parceiro, etc.).
Classe estendebr.com.sankhya.jape.event.PersistenceEventAdapter e sobrescreve só os eventos de interesse.
Listener é entrypoint fino: filtra o evento, manipula o DynamicVO e delega a regra de negócio a service/use case injetado.
3. Atributo da anotação @Listener
Atributo
Tipo
Obrigatório
Descrição
instanceNames
String[]
Sim
Nome(s) da(s) instância(s) a escutar — o mesmo valor de @JapeEntity(entity = "..."), de <instance name="..."> no XML, ou o nome da instância nativa. Não é o nome da tabela.
@Listener(instanceNames = "TdcXyzPedido")// uma instancia@Listener(instanceNames = {"CabecalhoNota", "Financeiro"})// varias instancias (nativas)
Gotcha:instanceNames é o nome lógico da entidade, não a tabela. Para @JapeEntity(entity = "TdcXyzPedido", table = "TDCXYZPED"), usa-se @Listener(instanceNames = "TdcXyzPedido").
4. Eventos disponíveis (PersistenceEventAdapter)
Método
Momento
Uso típico
beforeInsert(PersistenceEvent)
Antes de inserir
Preencher/validar campos; exceção bloqueia a inserção.
afterInsert(PersistenceEvent)
Depois de inserir
Auditoria, enfileirar processamento, propagar para outra entidade.
beforeUpdate(PersistenceEvent)
Antes de atualizar
Recalcular campos derivados; validar transição de status.
afterUpdate(PersistenceEvent)
Depois de atualizar
Reagir a mudança efetivada (ex.: status mudou para "Enviado").
beforeDelete(PersistenceEvent)
Antes de excluir
Impedir exclusão (exceção bloqueia); limpeza de dependências.
afterDelete(PersistenceEvent)
Depois de excluir
Auditoria de exclusão, propagação.
Todos declaram throws Exception. Em before*, exceção lançada cancela a operação e propaga a mensagem ao usuário — use exceção tipada com mensagem de negócio.
Alterações no DynamicVO feitas em before* são persistidas junto com a operação — não chame save.
Em after* o registro já foi gravado — alterar o VO ali não persiste nada.
O adapter também expõe hooks avançados (afterLoadValueObject, afterRetrieveValueObject, updateRequired, transferData) — raramente necessários.
5. API do PersistenceEvent
Método
Retorno
Uso
getVo()
EntityVO — cast para DynamicVO
Dados atuais do registro (ler/alterar campos).
getOldVO()
EntityVO
Dados antes da modificação (updates).
getModifingFields()
ModifingFields
Quais campos estão sendo alterados (updates) — ver §6.
getJdbcWrapper()
br.com.sankhya.jape.dao.JdbcWrapper
JDBC dentro da transação corrente. Nunca feche a conexão.
getEntity()
EntityMetaData
Metadados da entidade (getEntity().getName(), etc.).
Gotcha de pacote:JdbcWrapper do evento é br.com.sankhya.jape.dao.JdbcWrapper — nãobr.com.sankhya.jape.util.
Métodos úteis do DynamicVO
Método
Uso
getProperty("CAMPO")
Lê valor cru (Object, null se ausente).
setProperty("CAMPO", valor)
Altera campo — em before*, persiste junto com a operação.
asBigDecimalOrZero("CAMPO")
BigDecimal (zero se nulo) — ideal para cálculo.
asBigDecimal / asString / asInt
Conversões tipadas.
Do VO à entidade tipada — EntityMapper.fromVO
Para trabalhar com a entidade @JapeEntity em vez de strings de campo:
import br.com.sankhya.sdk.data.repository.impl.EntityMapper;
TdcXyzPedidopedido= EntityMapper.fromVO(event.getVo(), TdcXyzPedido.class);
if (!pedido.deveProcessar()) return; // regra de dominio na entidade, nao no listener
6. ModifingFields — filtrar updates por campo alterado
beforeUpdate/afterUpdate disparam em qualquer update da instância — e o evento de update só traz os campos alterados no VO. Filtre pelo que mudou:
Método
Uso
isModifing("CAMPO")
true se o campo está sendo alterado neste update.
isModifingAny("C1,C2")
true se qualquer um dos campos está sendo alterado.
getOldValue("CAMPO")
Valor anterior do campo.
getNewValue("CAMPO")
Valor novo do campo.
@OverridepublicvoidbeforeUpdate(PersistenceEvent event)throws Exception {
DynamicVOvo= (DynamicVO) event.getVo();
// Preenche vendedor preferencial so quando CODPARC muda sem CODVEND explicitoif (event.getModifingFields().isModifing("CODPARC")
&& !event.getModifingFields().isModifing("CODVEND")) {
preencherVendedorDoParceiro(vo);
}
}
Gotcha: no update, vo.getProperty("CAMPO") de campo não alterado pode vir null — o VO do evento só carrega o delta. Precisa do registro completo em after*? Leia a PK do VO e recarregue via repository/use case.
7. Injeção de dependência
@Listenersuporta @Inject (Guice) — não consta na documentação oficial, mas é suportado pelo SDK. Delegue a regra de negócio a services/use cases injetados via construtor:
@Log@Listener(instanceNames = "TdcXyzPedido")publicclassTdcXyzPedidoListenerextendsPersistenceEventAdapter {
privatefinal LimiteCreditoService limiteService;
@InjectpublicTdcXyzPedidoListener(LimiteCreditoService limiteService) {
this.limiteService = limiteService;
}
@OverridepublicvoidbeforeInsert(PersistenceEvent event)throws Exception {
DynamicVOvo= (DynamicVO) event.getVo();
// Excecao tipada do service bloqueia a gravacao com mensagem de negocio
limiteService.validar(vo.asBigDecimalOrZero("CODPARC"), vo.asBigDecimalOrZero("VLRTOT"));
}
}
@Inject de com.google.inject.Inject — nunca javax.inject.Inject.
Dependências private final, injetadas via construtor. Nunca new em dependência gerenciada.
Services/repositories registrados no módulo Guice do projeto (ver dependency-injection).
8. Transação, loops e chamadas externas
O listener roda dentro da transação da operação:
Exceção em before* → operação cancelada (rollback) e mensagem propagada ao usuário.
Acesso a banco na mesma transação: use event.getJdbcWrapper() ou repositories — nunca abra/feche conexão própria.
Chamada externa síncrona (API HTTP) dentro do listener é PROIBIDA — segura a transação e derruba o tempo de resposta da gravação. Padrão correto: gravar numa tabela-fila e processar via @Job ou worker pool assíncrono (fire-and-forget).
Loop de eventos
Listener que grava na própria instância que escuta (direta ou indiretamente via service) dispara os eventos de novo → loop infinito. Sempre proteja com guard clause de estado:
@OverridepublicvoidafterUpdate(PersistenceEvent event)throws Exception {
DynamicVOvo= (DynamicVO) event.getVo();
// O service muda o STATUS do proprio registro — sem este guard,// o update do service dispararia este afterUpdate de novo (loop)if (!"P".equals(vo.asString("STATUS"))) return;
processamentoService.processar(vo.asBigDecimal("NUPED"));
}
9. Anti-Patterns (PROIBIDO)
Anti-Pattern
Correção
Regra de negócio dentro do listener
Listener filtra evento e delega a service/use case injetado
Usar getVo() sem cast
(DynamicVO) event.getVo()
Update sem filtrar por getModifingFields().isModifing(...)
Filtrar campo alterado — listener dispara em qualquer update
Ler campo não alterado do VO em update esperando valor
VO de update só traz o delta — recarregar pela PK se precisar do todo
Chamada HTTP/API externa síncrona no listener
Tabela-fila + @Job/worker assíncrono
Gravar na própria instância sem guard clause de estado
Guard clause anti-loop (ver §8)
Abrir/fechar conexão JDBC própria
event.getJdbcWrapper() — nunca fechar
Alterar VO em after* esperando persistir
Alterações persistem só em before*
throw new RuntimeException(...) cru para bloquear operação