Skip to content

Instantly share code, notes, and snippets.

@wellington1993
Last active September 25, 2025 19:59
Show Gist options
  • Select an option

  • Save wellington1993/82fc7498ae4b437a04181381f95006d1 to your computer and use it in GitHub Desktop.

Select an option

Save wellington1993/82fc7498ae4b437a04181381f95006d1 to your computer and use it in GitHub Desktop.

Auditoria de Operações com UUID, Diff Real e Campos em Português

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.


📚 Índice


1. Estrutura do banco relacional (PostgreSQL)

-- 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 mesmo uuid_evento.


2. Entidades

2.1 Entidades de Negócio (Ex: Cliente, Pedido)

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;
}

2.2 Entidade de Log

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
}

3. Contexto para armazenar UUID

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(); }
}

4. AOP para gerar UUID automaticamente por requisição

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;
    }
}

5. Listener de auditoria com diff real

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;
    }
}

6. Fluxo visual (Mermaid)

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];
Loading

✅ Resumo

  • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment