Este guia mostra como auditar automaticamente operações de INSERT/UPDATE em uma aplicação Spring Boot com Hibernate e Lombok, usando campos em português. O destino deste documento é um Gist no GitHub.
- 1. Estrutura do banco relacional (PostgreSQL)
- 2. Entidades
- 3. Contexto para armazenar UUID
- 4. AOP para gerar UUID automaticamente por requisição
- 5. Listener de auditoria com diff real
- 6. Fluxo visual (Mermaid)
- ✅ Resumo
-- Tabela para os metadados da requisição
CREATE TABLE log_evento (
id BIGSERIAL PRIMARY KEY,
uuid_evento UUID NOT NULL UNIQUE,
endpoint VARCHAR(255),
id_usuario BIGINT,
status VARCHAR(50),
data_hora_inicio TIMESTAMP,
data_hora_fim TIMESTAMP
);
-- Adição da coluna de rastreabilidade nas tabelas de negócio
ALTER TABLE cliente ADD COLUMN uuid_evento UUID;
ALTER TABLE pedido ADD COLUMN uuid_evento UUID;
⚠️ Todas as alterações realizadas em uma requisição terão o mesmouuid_evento.
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.envers.Audited;
import java.util.UUID;
@Entity
@Audited
@Data
public class Cliente {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nome;
private String email;
@Column(name = "uuid_evento")
private UUID uuidEvento;
}
@Entity
@Audited
@Data
public class Pedido {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long idCliente;
private String status;
@Column(name = "uuid_evento")
private UUID uuidEvento;
}import jakarta.persistence.*;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
@Table(name = "log_evento")
@Data
public class LogEvento {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private UUID uuidEvento;
private String endpoint;
private Long idUsuario;
@Enumerated(EnumType.STRING)
private LogStatus status;
private LocalDateTime dataHoraInicio;
private LocalDateTime dataHoraFim;
}
public enum LogStatus {
PENDENTE,
CONCLUIDO,
ERRO
}import java.util.UUID;
public class ContextoEvento {
private static final ThreadLocal<UUID> eventoAtual = new ThreadLocal<>();
public static void set(UUID uuid) { eventoAtual.set(uuid); }
public static UUID get() { return eventoAtual.get(); }
public static void clear() { eventoAtual.remove(); }
}Primeiro, o repositório para a entidade LogEvento:
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;
public interface LogEventoRepository extends JpaRepository<LogEvento, Long> {
Optional<LogEvento> findByUuidEvento(UUID uuid);
}Agora, o Aspecto com a lógica de captura do usuário logado preenchida:
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.UUID;
@Aspect
@Component
public class AspectoEvento {
private final LogEventoRepository logEventoRepository;
private final HttpServletRequest request;
public AspectoEvento(LogEventoRepository logEventoRepository, HttpServletRequest request) {
this.logEventoRepository = logEventoRepository;
this.request = request;
}
@Around("within(@org.springframework.web.bind.annotation.RestController *)")
public Object wrapController(ProceedingJoinPoint joinPoint) throws Throwable {
UUID uuidEvento = UUID.randomUUID();
ContextoEvento.set(uuidEvento);
LogEvento log = new LogEvento();
log.setUuidEvento(uuidEvento);
log.setEndpoint(request.getRequestURI());
log.setStatus(LogStatus.PENDENTE);
log.setDataHoraInicio(LocalDateTime.now());
log.setIdUsuario(getUsuarioLogadoId());
logEventoRepository.save(log);
try {
Object resultado = joinPoint.proceed();
atualizarStatusLog(uuidEvento, LogStatus.CONCLUIDO);
return resultado;
} catch (Throwable e) {
atualizarStatusLog(uuidEvento, LogStatus.ERRO);
throw e;
} finally {
ContextoEvento.clear();
}
}
private void atualizarStatusLog(UUID uuid, LogStatus status) {
logEventoRepository.findByUuidEvento(uuid).ifPresent(log -> {
log.setStatus(status);
log.setDataHoraFim(LocalDateTime.now());
logEventoRepository.save(log);
});
}
/**
* Obtém o ID do usuário a partir do SecurityContext.
* Baseado na sua implementação de segurança, o 'principal' é o ID do usuário como String.
*/
private Long getUsuarioLogadoId() {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
Object principal = authentication.getPrincipal();
if (principal instanceof String) {
// Evita NumberFormatException para principals como "anonymousUser"
if (!((String) principal).equalsIgnoreCase("anonymousUser")) {
return Long.parseLong((String) principal);
}
}
}
} catch (Exception e) {
// Se ocorrer qualquer erro (ex: parsing, cast), retorna null.
// Isso é seguro para rotas públicas onde não há usuário.
return null;
}
return null;
}
}import jakarta.persistence.*;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.AuditReaderFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Component
public class ListenerAuditoria {
@PersistenceContext
private EntityManager em;
private static MongoTemplate mongoTemplate;
@Autowired
public void setMongoTemplate(MongoTemplate template) {
ListenerAuditoria.mongoTemplate = template;
}
@PostPersist
@PostUpdate
public void aoSalvarOuAtualizar(Object entidade) {
UUID uuidEvento = ContextoEvento.get();
if(uuidEvento == null) return;
setUuidNaEntidade(entidade, uuidEvento);
Map<String, Map<String,Object>> alteracoes = calcularDiff(entidade);
if (alteracoes.isEmpty()) return;
Map<String,Object> doc = new HashMap<>();
doc.put("uuid_evento", uuidEvento.toString());
doc.put("entidade", entidade.getClass().getSimpleName());
doc.put("id_entidade", getIdEntidade(entidade));
doc.put("alteracoes", alteracoes);
doc.put("data_hora", LocalDateTime.now());
mongoTemplate.insert(doc, "logs_detalhados");
}
private void setUuidNaEntidade(Object entidade, UUID uuid) {
try {
Method setter = entidade.getClass().getMethod("setUuidEvento", UUID.class);
setter.invoke(entidade, uuid);
} catch (Exception e) {
// A entidade pode não ter o campo uuidEvento, ignorar silenciosamente
}
}
private Object getIdEntidade(Object entidade) {
try {
return entidade.getClass().getMethod("getId").invoke(entidade);
} catch (Exception e) {
return null;
}
}
private Map<String, Map<String,Object>> calcularDiff(Object entidade) {
Map<String, Map<String,Object>> diff = new HashMap<>();
AuditReader reader = AuditReaderFactory.get(em);
Object id = getIdEntidade(entidade);
if (id == null) return diff; // Não auditar se não tiver ID
var revisoes = reader.getRevisions(entidade.getClass(), id);
try {
if (revisoes.size() < 2) {
// Para uma nova entidade, todos os campos são considerados "novos"
for (Field field : entidade.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object valorNovo = field.get(entidade);
if (valorNovo != null) {
Map<String, Object> alteracao = new HashMap<>();
alteracao.put("antigo", null);
alteracao.put("novo", valorNovo);
diff.put(field.getName(), alteracao);
}
}
} else {
// Lógica para updates
Number revAnteriorNum = revisoes.get(revisoes.size() - 2);
Object entidadeAnterior = reader.find(entidade.getClass(), id, revAnteriorNum);
for (Field field : entidade.getClass().getDeclaredFields()) {
field.setAccessible(true);
Object valorAntigo = entidadeAnterior != null ? field.get(entidadeAnterior) : null;
Object valorNovo = field.get(entidade);
if ((valorAntigo != null && !valorAntigo.equals(valorNovo)) || (valorAntigo == null && valorNovo != null)) {
Map<String, Object> alteracao = new HashMap<>();
alteracao.put("antigo", valorAntigo);
alteracao.put("novo", valorNovo);
diff.put(field.getName(), alteracao);
}
}
}
} catch (Exception e) {
// Logar erro, se necessário
}
return diff;
}
}flowchart TD
A[Requisição chega ao Endpoint] --> B{Aspecto AOP intercepta};
B --> C[Gera UUID do Evento];
C --> J[Salva log inicial no PostgreSQL com status PENDENTE e ID do Usuário];
J --> D[Armazena UUID no ThreadLocal];
D --> E[Executa a lógica de negócio];
E --> F{INSERT/UPDATE de Entidades};
F --> K[Entidade recebe o UUID do evento];
K --> G[Listener de Auditoria é acionado];
G --> H[Calcula 'diff' usando Envers];
H --> I[Salva log detalhado no MongoDB];
I --> L[Controller retorna a resposta];
L --> M{Bloco Finally do Aspecto AOP};
M --> U[Atualiza log no PostgreSQL para CONCLUÍDO/ERRO];
U --> N[Limpa o ThreadLocal];
- Rastreabilidade: UUID por requisição garante o rastreamento completo de uma operação.
- Auditoria Automática: O listener calcula as diferenças reais entre o estado antigo e novo da entidade.
- Flexibilidade: Logs detalhados podem ser enviados para MongoDB ou armazenados em um campo JSONB no PostgreSQL.
- Baixo Acoplamento: O sistema funciona automaticamente sem a necessidade de chamadas manuais nos serviços ou controllers.
- Nomenclatura: O uso de tabelas e campos em português é perfeitamente suportado.
- Integração com Segurança: Captura o ID do usuário autenticado via JWT e o associa ao log do evento.