在企业级Java应用开发过程中,数据的可追溯性和变更历史记录是确保系统可靠性和合规性的关键要素。审计功能的实现需要记录数据的创建时间、修改时间以及相关的操作人员信息,这些信息对于业务决策、问题排查和监管合规具有重要价值。Spring Data JPA提供的@CreatedDate、@LastModifiedDate、@CreatedBy和@LastModifiedBy注解为开发者提供了优雅的自动化审计解决方案。这些注解通过JPA的生命周期回调机制,能够在实体创建和更新时自动填充审计字段,消除了手动维护这些信息的复杂性和错误风险。通过合理配置和使用这些审计注解,开发者可以构建完整的数据变更追踪体系,为企业级应用的数据治理提供坚实的技术基础。
Spring Data JPA的审计功能基于JPA实体监听器机制实现,通过AuditingEntityListener监听器自动捕获实体的生命周期事件。@CreatedDate和@LastModifiedDate注解分别用于标记创建时间和最后修改时间字段,这些字段会在实体持久化和更新操作时自动设置相应的时间戳。审计功能的激活需要在配置类上添加@EnableJpaAuditing注解,并确保实体类使用@EntityListeners注解引入AuditingEntityListener监听器。
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditAwareImpl")
public class JpaAuditingConfiguration {
@Bean
public AuditorAware<String> auditAwareImpl() {
return new AuditorAwareImpl();
}
}
@Component
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// 从Spring Security上下文获取当前用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated() ||
authentication instanceof AnonymousAuthenticationToken) {
return Optional.of("system");
}
return Optional.of(authentication.getName());
}
}
@Entity
@Table(name = "articles")
@EntityListeners(AuditingEntityListener.class)
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@Column(name = "author_id")
private Long authorId;
@Enumerated(EnumType.STRING)
private ArticleStatus status;
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "last_modified_at", nullable = false)
private LocalDateTime lastModifiedAt;
@CreatedBy
@Column(name = "created_by", nullable = false, updatable = false)
private String createdBy;
@LastModifiedBy
@Column(name = "last_modified_by", nullable = false)
private String lastModifiedBy;
@Version
private Long version;
// 构造方法、getter和setter省略
}
@Service
public class ArticleService {
@Autowired
private ArticleRepository articleRepository;
public Article createArticle(ArticleCreateDto createDto) {
Article article = new Article();
article.setTitle(createDto.getTitle());
article.setContent(createDto.getContent());
article.setAuthorId(createDto.getAuthorId());
article.setStatus(ArticleStatus.DRAFT);
// 审计字段会自动填充
// createdAt和lastModifiedAt会设置为当前时间
// createdBy和lastModifiedBy会从AuditorAware实现中获取
Article savedArticle = articleRepository.save(article);
return savedArticle;
}
public Article updateArticle(Long articleId, ArticleUpdateDto updateDto) {
Article article = articleRepository.findById(articleId)
.orElseThrow(() -> new EntityNotFoundException("Article not found"));
article.setTitle(updateDto.getTitle());
article.setContent(updateDto.getContent());
article.setStatus(updateDto.getStatus());
// 只有lastModifiedAt和lastModifiedBy会自动更新
// createdAt和createdBy保持不变
return articleRepository.save(article);
}
public ArticleAuditInfo getArticleAuditInfo(Long articleId) {
Article article = articleRepository.findById(articleId)
.orElseThrow(() -> new EntityNotFoundException("Article not found"));
return new ArticleAuditInfo(
article.getCreatedAt(),
article.getCreatedBy(),
article.getLastModifiedAt(),
article.getLastModifiedBy(),
article.getVersion()
);
}
}
审计功能中的时间字段支持多种Java时间类型,包括LocalDateTime、Instant、ZonedDateTime等。不同的时间类型在处理时区和精度方面具有不同的特性,开发者需要根据应用的具体需求选择合适的时间类型。对于全球化应用,建议使用Instant或ZonedDateTime来确保时间的一致性和准确性。时区配置可以通过应用配置或自定义的DateTimeProvider来实现。
@Entity
@Table(name = "global_events")
@EntityListeners(AuditingEntityListener.class)
public class GlobalEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String eventName;
private String description;
@Column(name = "event_location")
private String eventLocation;
@Column(name = "time_zone")
private String timeZone;
// 使用Instant确保UTC时间存储
@CreatedDate
@Column(name = "created_at_utc", nullable = false, updatable = false)
private Instant createdAtUtc;
@LastModifiedDate
@Column(name = "last_modified_at_utc", nullable = false)
private Instant lastModifiedAtUtc;
// 使用LocalDateTime存储本地时间
@CreatedDate
@Column(name = "created_at_local", nullable = false, updatable = false)
private LocalDateTime createdAtLocal;
@LastModifiedDate
@Column(name = "last_modified_at_local", nullable = false)
private LocalDateTime lastModifiedAtLocal;
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;
@LastModifiedBy
@Column(name = "last_modified_by")
private String lastModifiedBy;
// 构造方法、getter和setter省略
}
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditAwareImpl", dateTimeProviderRef = "dateTimeProvider")
public class AdvancedJpaAuditingConfiguration {
@Bean
public DateTimeProvider dateTimeProvider() {
return new CustomDateTimeProvider();
}
@Bean
public AuditorAware<String> auditAwareImpl() {
return new EnhancedAuditorAwareImpl();
}
}
@Component
public class CustomDateTimeProvider implements DateTimeProvider {
@Override
public Optional<TemporalAccessor> getNow() {
// 返回当前UTC时间
return Optional.of(Instant.now());
}
}
@Component
public class EnhancedAuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.of("anonymous");
}
// 获取用户详细信息
if (authentication.getPrincipal() instanceof UserDetails) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return Optional.of(userDetails.getUsername());
}
return Optional.of(authentication.getName());
}
}
@Service
public class GlobalEventService {
@Autowired
private GlobalEventRepository eventRepository;
public GlobalEvent createEvent(GlobalEventCreateDto createDto) {
GlobalEvent event = new GlobalEvent();
event.setEventName(createDto.getEventName());
event.setDescription(createDto.getDescription());
event.setEventLocation(createDto.getEventLocation());
event.setTimeZone(createDto.getTimeZone());
return eventRepository.save(event);
}
public List<EventAuditSummary> getEventsWithAuditInfo(String location) {
List<GlobalEvent> events = eventRepository.findByEventLocation(location);
return events.stream()
.map(this::convertToAuditSummary)
.collect(Collectors.toList());
}
private EventAuditSummary convertToAuditSummary(GlobalEvent event) {
return new EventAuditSummary(
event.getId(),
event.getEventName(),
event.getCreatedAtUtc(),
event.getLastModifiedAtUtc(),
event.getCreatedBy(),
event.getLastModifiedBy(),
calculateTimeSinceCreation(event.getCreatedAtUtc()),
calculateTimeSinceLastModification(event.getLastModifiedAtUtc())
);
}
private Duration calculateTimeSinceCreation(Instant createdAt) {
return Duration.between(createdAt, Instant.now());
}
private Duration calculateTimeSinceLastModification(Instant lastModifiedAt) {
return Duration.between(lastModifiedAt, Instant.now());
}
}
// 审计信息传输对象
public class EventAuditSummary {
private Long eventId;
private String eventName;
private Instant createdAtUtc;
private Instant lastModifiedAtUtc;
private String createdBy;
private String lastModifiedBy;
private Duration timeSinceCreation;
private Duration timeSinceLastModification;
// 构造方法、getter和setter省略
}
除了标准的时间和用户审计字段外,企业级应用通常需要记录更多的上下文信息,如客户端IP地址、用户代理信息、操作类型等。这些自定义审计字段需要通过扩展审计监听器或使用JPA生命周期回调方法来实现。复杂的审计场景还可能涉及条件性审计、批量操作审计和异步审计处理等高级功能。
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class ExtendedAuditableEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "last_modified_at", nullable = false)
private LocalDateTime lastModifiedAt;
@CreatedBy
@Column(name = "created_by", updatable = false)
private String createdBy;
@LastModifiedBy
@Column(name = "last_modified_by")
private String lastModifiedBy;
@Column(name = "created_ip", updatable = false)
private String createdIp;
@Column(name = "last_modified_ip")
private String lastModifiedIp;
@Column(name = "created_user_agent", updatable = false, length = 500)
private String createdUserAgent;
@Column(name = "last_modified_user_agent", length = 500)
private String lastModifiedUserAgent;
@Column(name = "audit_version")
private Integer auditVersion = 1;
@PrePersist
protected void prePersist() {
AuditContext auditContext = AuditContextHolder.getContext();
if (auditContext != null) {
this.createdIp = auditContext.getIpAddress();
this.createdUserAgent = auditContext.getUserAgent();
this.lastModifiedIp = auditContext.getIpAddress();
this.lastModifiedUserAgent = auditContext.getUserAgent();
}
}
@PreUpdate
protected void preUpdate() {
AuditContext auditContext = AuditContextHolder.getContext();
if (auditContext != null) {
this.lastModifiedIp = auditContext.getIpAddress();
this.lastModifiedUserAgent = auditContext.getUserAgent();
}
this.auditVersion = (this.auditVersion == null ? 1 : this.auditVersion) + 1;
}
// getter和setter省略
}
@Entity
@Table(name = "sensitive_documents")
public class SensitiveDocument extends ExtendedAuditableEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(columnDefinition = "TEXT")
private String content;
@Column(name = "classification_level")
@Enumerated(EnumType.STRING)
private ClassificationLevel classificationLevel;
@Column(name = "access_count", nullable = false)
private Integer accessCount = 0;
@Column(name = "last_accessed_at")
private LocalDateTime lastAccessedAt;
@Column(name = "last_accessed_by")
private String lastAccessedBy;
// 构造方法、getter和setter省略
}
@Component
public class AuditContextHolder {
private static final ThreadLocal<AuditContext> contextHolder = new ThreadLocal<>();
public static void setContext(AuditContext context) {
contextHolder.set(context);
}
public static AuditContext getContext() {
return contextHolder.get();
}
public static void clearContext() {
contextHolder.remove();
}
}
public class AuditContext {
private String ipAddress;
private String userAgent;
private String sessionId;
private String operationType;
private Map<String, Object> additionalInfo;
public AuditContext(String ipAddress, String userAgent, String sessionId) {
this.ipAddress = ipAddress;
this.userAgent = userAgent;
this.sessionId = sessionId;
this.additionalInfo = new HashMap<>();
}
// getter和setter省略
}
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
@Autowired
private SensitiveDocumentService documentService;
@PostMapping
public ResponseEntity<SensitiveDocument> createDocument(
@RequestBody DocumentCreateDto createDto,
HttpServletRequest request) {
// 设置审计上下文
AuditContext auditContext = new AuditContext(
getClientIpAddress(request),
request.getHeader("User-Agent"),
request.getSession().getId()
);
auditContext.setOperationType("CREATE_DOCUMENT");
AuditContextHolder.setContext(auditContext);
try {
SensitiveDocument document = documentService.createDocument(createDto);
return ResponseEntity.ok(document);
} finally {
AuditContextHolder.clearContext();
}
}
@PutMapping("/{id}")
public ResponseEntity<SensitiveDocument> updateDocument(
@PathVariable Long id,
@RequestBody DocumentUpdateDto updateDto,
HttpServletRequest request) {
AuditContext auditContext = new AuditContext(
getClientIpAddress(request),
request.getHeader("User-Agent"),
request.getSession().getId()
);
auditContext.setOperationType("UPDATE_DOCUMENT");
auditContext.getAdditionalInfo().put("documentId", id);
AuditContextHolder.setContext(auditContext);
try {
SensitiveDocument document = documentService.updateDocument(id, updateDto);
return ResponseEntity.ok(document);
} finally {
AuditContextHolder.clearContext();
}
}
@GetMapping("/{id}")
public ResponseEntity<SensitiveDocument> getDocument(
@PathVariable Long id,
HttpServletRequest request) {
AuditContext auditContext = new AuditContext(
getClientIpAddress(request),
request.getHeader("User-Agent"),
request.getSession().getId()
);
auditContext.setOperationType("ACCESS_DOCUMENT");
AuditContextHolder.setContext(auditContext);
try {
SensitiveDocument document = documentService.getDocumentWithAccessTracking(id);
return ResponseEntity.ok(document);
} finally {
AuditContextHolder.clearContext();
}
}
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedForHeader = request.getHeader("X-Forwarded-For");
if (xForwardedForHeader == null) {
return request.getRemoteAddr();
} else {
return xForwardedForHeader.split(",")[0];
}
}
}
@Service
public class SensitiveDocumentService {
@Autowired
private SensitiveDocumentRepository documentRepository;
public SensitiveDocument createDocument(DocumentCreateDto createDto) {
SensitiveDocument document = new SensitiveDocument();
document.setTitle(createDto.getTitle());
document.setContent(createDto.getContent());
document.setClassificationLevel(createDto.getClassificationLevel());
return documentRepository.save(document);
}
public SensitiveDocument updateDocument(Long documentId, DocumentUpdateDto updateDto) {
SensitiveDocument document = documentRepository.findById(documentId)
.orElseThrow(() -> new EntityNotFoundException("Document not found"));
document.setTitle(updateDto.getTitle());
document.setContent(updateDto.getContent());
document.setClassificationLevel(updateDto.getClassificationLevel());
return documentRepository.save(document);
}
@Transactional
public SensitiveDocument getDocumentWithAccessTracking(Long documentId) {
SensitiveDocument document = documentRepository.findById(documentId)
.orElseThrow(() -> new EntityNotFoundException("Document not found"));
// 更新访问统计但不触发审计字段更新
documentRepository.incrementAccessCount(documentId);
documentRepository.updateLastAccessed(documentId, getCurrentUser());
return document;
}
private String getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return authentication != null ? authentication.getName() : "anonymous";
}
}
审计数据的价值不仅在于记录,更重要的是为业务分析和监控提供支持。通过构建专门的审计查询接口和分析工具,可以实现对数据变更模式的深入洞察。这些功能包括变更频率分析、用户行为追踪、数据完整性验证等多个维度的审计报告生成。
@Repository
public interface SensitiveDocumentRepository extends JpaRepository<SensitiveDocument, Long> {
@Modifying
@Query("UPDATE SensitiveDocument d SET d.accessCount = d.accessCount + 1 WHERE d.id = :documentId")
void incrementAccessCount(@Param("documentId") Long documentId);
@Modifying
@Query("UPDATE SensitiveDocument d SET d.lastAccessedAt = CURRENT_TIMESTAMP, d.lastAccessedBy = :userId WHERE d.id = :documentId")
void updateLastAccessed(@Param("documentId") Long documentId, @Param("userId") String userId);
@Query("SELECT d FROM SensitiveDocument d WHERE d.createdBy = :userId ORDER BY d.createdAt DESC")
List<SensitiveDocument> findByCreatedBy(@Param("userId") String userId);
@Query("SELECT d FROM SensitiveDocument d WHERE d.lastModifiedBy = :userId AND d.lastModifiedAt >= :since ORDER BY d.lastModifiedAt DESC")
List<SensitiveDocument> findRecentlyModifiedBy(@Param("userId") String userId, @Param("since") LocalDateTime since);
@Query("SELECT d FROM SensitiveDocument d WHERE d.createdAt BETWEEN :startDate AND :endDate")
List<SensitiveDocument> findCreatedBetween(@Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate);
@Query("""
SELECT new com.example.dto.DocumentAuditStatistics(
d.createdBy,
COUNT(d),
MIN(d.createdAt),
MAX(d.lastModifiedAt),
AVG(d.accessCount)
)
FROM SensitiveDocument d
GROUP BY d.createdBy
ORDER BY COUNT(d) DESC
""")
List<DocumentAuditStatistics> getDocumentStatisticsByUser();
@Query(value = """
SELECT
DATE(created_at) as date,
created_by,
COUNT(*) as documents_created,
AVG(access_count) as avg_access_count
FROM sensitive_documents
WHERE created_at >= :startDate
GROUP BY DATE(created_at), created_by
ORDER BY date DESC, documents_created DESC
""", nativeQuery = true)
List<Object[]> getDailyCreationStatistics(@Param("startDate") LocalDateTime startDate);
}
@Service
public class DocumentAuditService {
@Autowired
private SensitiveDocumentRepository documentRepository;
public List<DocumentAuditStatistics> getUserDocumentStatistics() {
return documentRepository.getDocumentStatisticsByUser();
}
public DocumentActivityReport generateActivityReport(String userId, int days) {
LocalDateTime since = LocalDateTime.now().minusDays(days);
List<SensitiveDocument> createdDocuments = documentRepository.findByCreatedBy(userId);
List<SensitiveDocument> modifiedDocuments = documentRepository.findRecentlyModifiedBy(userId, since);
DocumentActivityReport report = new DocumentActivityReport();
report.setUserId(userId);
report.setReportPeriodDays(days);
report.setTotalDocumentsCreated(createdDocuments.size());
report.setRecentlyModifiedDocuments(modifiedDocuments.size());
// 计算平均访问次数
double averageAccessCount = createdDocuments.stream()
.mapToInt(SensitiveDocument::getAccessCount)
.average()
.orElse(0.0);
report.setAverageAccessCount(averageAccessCount);
// 分析文档分类分布
Map<ClassificationLevel, Long> classificationDistribution = createdDocuments.stream()
.collect(Collectors.groupingBy(
SensitiveDocument::getClassificationLevel,
Collectors.counting()
));
report.setClassificationDistribution(classificationDistribution);
// 计算活跃度指标
long recentModifications = modifiedDocuments.stream()
.filter(doc -> doc.getLastModifiedAt().isAfter(since))
.count();
report.setActivityScore(calculateActivityScore(createdDocuments.size(), recentModifications));
return report;
}
public SystemAuditReport generateSystemAuditReport(LocalDateTime startDate, LocalDateTime endDate) {
List<SensitiveDocument> documentsInPeriod = documentRepository.findCreatedBetween(startDate, endDate);
List<Object[]> dailyStatistics = documentRepository.getDailyCreationStatistics(startDate);
SystemAuditReport report = new SystemAuditReport();
report.setReportPeriodStart(startDate);
report.setReportPeriodEnd(endDate);
report.setTotalDocuments(documentsInPeriod.size());
// 分析用户活动模式
Map<String, Long> userActivityMap = documentsInPeriod.stream()
.collect(Collectors.groupingBy(
SensitiveDocument::getCreatedBy,
Collectors.counting()
));
report.setUserActivitySummary(userActivityMap);
// 分析时间分布模式
Map<DayOfWeek, Long> dayOfWeekDistribution = documentsInPeriod.stream()
.collect(Collectors.groupingBy(
doc -> doc.getCreatedAt().getDayOfWeek(),
Collectors.counting()
));
report.setDayOfWeekDistribution(dayOfWeekDistribution);
// 分析小时分布模式
Map<Integer, Long> hourlyDistribution = documentsInPeriod.stream()
.collect(Collectors.groupingBy(
doc -> doc.getCreatedAt().getHour(),
Collectors.counting()
));
report.setHourlyDistribution(hourlyDistribution);
// 计算系统健康指标
report.setSystemHealthScore(calculateSystemHealthScore(documentsInPeriod));
return report;
}
public List<DocumentChangePattern> analyzeChangePatterns(int analysisPeriodDays) {
LocalDateTime analysisStart = LocalDateTime.now().minusDays(analysisPeriodDays);
List<SensitiveDocument> documents = documentRepository.findCreatedBetween(analysisStart, LocalDateTime.now());
return documents.stream()
.map(this::analyzeDocumentChangePattern)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
private DocumentChangePattern analyzeDocumentChangePattern(SensitiveDocument document) {
Duration timeBetweenCreationAndLastModification = Duration.between(
document.getCreatedAt(),
document.getLastModifiedAt()
);
boolean isFrequentlyModified = timeBetweenCreationAndLastModification.toDays() < 1;
boolean isHighlyAccessed = document.getAccessCount() > 10;
if (isFrequentlyModified || isHighlyAccessed) {
return new DocumentChangePattern(
document.getId(),
document.getTitle(),
timeBetweenCreationAndLastModification,
document.getAccessCount(),
document.getAuditVersion(),
determineChangeCategory(document)
);
}
return null;
}
private double calculateActivityScore(int totalCreated, long recentModifications) {
if (totalCreated == 0) return 0.0;
return (recentModifications * 1.0 / totalCreated) * 100;
}
private double calculateSystemHealthScore(List<SensitiveDocument> documents) {
if (documents.isEmpty()) return 100.0;
double averageAccessCount = documents.stream()
.mapToInt(SensitiveDocument::getAccessCount)
.average()
.orElse(0.0);
long activeDocuments = documents.stream()
.filter(doc -> doc.getAccessCount() > 0)
.count();
double utilizationRate = (activeDocuments * 1.0 / documents.size()) * 100;
return Math.min(100.0, (utilizationRate + Math.min(averageAccessCount * 10, 50)) / 2);
}
private String determineChangeCategory(SensitiveDocument document) {
if (document.getAuditVersion() > 5) {
return "FREQUENTLY_UPDATED";
} else if (document.getAccessCount() > 20) {
return "HIGHLY_ACCESSED";
} else {
return "NORMAL";
}
}
}
// 数据传输对象
public class DocumentAuditStatistics {
private String userId;
private Long totalDocuments;
private LocalDateTime firstDocumentDate;
private LocalDateTime lastModificationDate;
private Double averageAccessCount;
// 构造方法、getter和setter省略
}
public class DocumentActivityReport {
private String userId;
private int reportPeriodDays;
private int totalDocumentsCreated;
private int recentlyModifiedDocuments;
private double averageAccessCount;
private Map<ClassificationLevel, Long> classificationDistribution;
private double activityScore;
// 构造方法、getter和setter省略
}
public class SystemAuditReport {
private LocalDateTime reportPeriodStart;
private LocalDateTime reportPeriodEnd;
private int totalDocuments;
private Map<String, Long> userActivitySummary;
private Map<DayOfWeek, Long> dayOfWeekDistribution;
private Map<Integer, Long> hourlyDistribution;
private double systemHealthScore;
// 构造方法、getter和setter省略
}
public class DocumentChangePattern {
private Long documentId;
private String documentTitle;
private Duration timeBetweenCreationAndLastModification;
private Integer accessCount;
private Integer auditVersion;
private String changeCategory;
// 构造方法、getter和setter省略
}
在复杂的企业环境中,审计功能需要支持多层级的权限控制和数据隔离。不同角色的用户应该能够访问不同级别的审计信息,管理员可以查看完整的审计历史,而普通用户只能查看自己相关的审计记录。这种细粒度的权限控制需要结合Spring Security和自定义的审计查询逻辑来实现。
@Entity
@Table(name = "audit_logs")
@EntityListeners(AuditingEntityListener.class)
public class AuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "entity_type", nullable = false)
private String entityType;
@Column(name = "entity_id", nullable = false)
private String entityId;
@Column(name = "operation_type", nullable = false)
@Enumerated(EnumType.STRING)
private OperationType operationType;
@Column(name = "old_values", columnDefinition = "TEXT")
private String oldValues;
@Column(name = "new_values", columnDefinition = "TEXT")
private String newValues;
@Column(name = "field_changes", columnDefinition = "TEXT")
private String fieldChanges;
@CreatedDate
@Column(name = "operation_timestamp", nullable = false)
private LocalDateTime operationTimestamp;
@CreatedBy
@Column(name = "performed_by", nullable = false)
private String performedBy;
@Column(name = "ip_address")
private String ipAddress;
@Column(name = "user_agent", length = 500)
private String userAgent;
@Column(name = "session_id")
private String sessionId;
@Column(name = "audit_level", nullable = false)
@Enumerated(EnumType.STRING)
private AuditLevel auditLevel;
@Column(name = "business_context", columnDefinition = "TEXT")
private String businessContext;
// 构造方法、getter和setter省略
}
public enum AuditLevel {
PUBLIC, // 所有用户可见
INTERNAL, // 内部用户可见
RESTRICTED, // 管理员可见
CONFIDENTIAL // 系统管理员可见
}
public enum OperationType {
CREATE, UPDATE, DELETE, VIEW, EXPORT, IMPORT, LOGIN, LOGOUT
}
@Service
public class AuditLogService {
@Autowired
private AuditLogRepository auditLogRepository;
@PreAuthorize("hasRole('USER')")
public List<AuditLog> getUserAuditHistory(String userId, int days) {
LocalDateTime since = LocalDateTime.now().minusDays(days);
// 普通用户只能查看自己的公开审计记录
return auditLogRepository.findByPerformedByAndAuditLevelAndOperationTimestampAfter(
userId, AuditLevel.PUBLIC, since);
}
@PreAuthorize("hasRole('MANAGER')")
public List<AuditLog> getTeamAuditHistory(List<String> teamMemberIds, int days) {
LocalDateTime since = LocalDateTime.now().minusDays(days);
// 管理者可以查看团队成员的内部级别审计记录
return auditLogRepository.findByPerformedByInAndAuditLevelInAndOperationTimestampAfter(
teamMemberIds,
Arrays.asList(AuditLevel.PUBLIC, AuditLevel.INTERNAL),
since);
}
@PreAuthorize("hasRole('ADMIN')")
public List<AuditLog> getSystemAuditHistory(int days) {
LocalDateTime since = LocalDateTime.now().minusDays(days);
// 系统管理员可以查看限制级别的审计记录
return auditLogRepository.findByAuditLevelInAndOperationTimestampAfter(
Arrays.asList(AuditLevel.PUBLIC, AuditLevel.INTERNAL, AuditLevel.RESTRICTED),
since);
}
@PreAuthorize("hasRole('SUPER_ADMIN')")
public List<AuditLog> getCompleteAuditHistory(int days) {
LocalDateTime since = LocalDateTime.now().minusDays(days);
// 超级管理员可以查看所有级别的审计记录
return auditLogRepository.findByOperationTimestampAfter(since);
}
public void createAuditLog(AuditLogCreateRequest request) {
AuditLog auditLog = new AuditLog();
auditLog.setEntityType(request.getEntityType());
auditLog.setEntityId(request.getEntityId());
auditLog.setOperationType(request.getOperationType());
auditLog.setOldValues(request.getOldValues());
auditLog.setNewValues(request.getNewValues());
auditLog.setFieldChanges(request.getFieldChanges());
auditLog.setAuditLevel(determineAuditLevel(request));
auditLog.setBusinessContext(request.getBusinessContext());
// 从审计上下文获取额外信息
AuditContext context = AuditContextHolder.getContext();
if (context != null) {
auditLog.setIpAddress(context.getIpAddress());
auditLog.setUserAgent(context.getUserAgent());
auditLog.setSessionId(context.getSessionId());
}
auditLogRepository.save(auditLog);
}
private AuditLevel determineAuditLevel(AuditLogCreateRequest request) {
// 根据实体类型和操作类型确定审计级别
if (request.getEntityType().contains("Sensitive") ||
request.getOperationType() == OperationType.DELETE) {
return AuditLevel.RESTRICTED;
} else if (request.getOperationType() == OperationType.EXPORT ||
request.getOperationType() == OperationType.IMPORT) {
return AuditLevel.INTERNAL;
} else {
return AuditLevel.PUBLIC;
}
}
}
@Repository
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
List<AuditLog> findByPerformedByAndAuditLevelAndOperationTimestampAfter(
String performedBy, AuditLevel auditLevel, LocalDateTime since);
List<AuditLog> findByPerformedByInAndAuditLevelInAndOperationTimestampAfter(
List<String> performedByList, List<AuditLevel> auditLevels, LocalDateTime since);
List<AuditLog> findByAuditLevelInAndOperationTimestampAfter(
List<AuditLevel> auditLevels, LocalDateTime since);
List<AuditLog> findByOperationTimestampAfter(LocalDateTime since);
List<AuditLog> findByEntityTypeAndEntityId(String entityType, String entityId);
@Query("""
SELECT new com.example.dto.AuditSummary(
a.entityType,
a.operationType,
COUNT(a),
MIN(a.operationTimestamp),
MAX(a.operationTimestamp)
)
FROM AuditLog a
WHERE a.operationTimestamp >= :startDate
AND a.auditLevel IN :allowedLevels
GROUP BY a.entityType, a.operationType
ORDER BY COUNT(a) DESC
""")
List<AuditSummary> getAuditSummary(@Param("startDate") LocalDateTime startDate,
@Param("allowedLevels") List<AuditLevel> allowedLevels);
}
// 审计日志创建请求类
public class AuditLogCreateRequest {
private String entityType;
private String entityId;
private OperationType operationType;
private String oldValues;
private String newValues;
private String fieldChanges;
private String businessContext;
// 构造方法、getter和setter省略
}
// 审计摘要数据传输对象
public class AuditSummary {
private String entityType;
private OperationType operationType;
private Long operationCount;
private LocalDateTime firstOperation;
private LocalDateTime lastOperation;
// 构造方法、getter和setter省略
}
随着系统运行时间的增长,审计数据会不断积累,可能对系统性能产生影响。企业级应用需要建立完善的审计数据归档策略,包括定期归档历史数据、优化查询性能和实现数据的生命周期管理。这些策略确保审计功能在长期运行中保持高效性能。
@Entity
@Table(name = "audit_archives")
public class AuditArchive {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "archive_date", nullable = false)
private LocalDate archiveDate;
@Column(name = "archived_records_count", nullable = false)
private Long archivedRecordsCount;
@Column(name = "archive_file_path")
private String archiveFilePath;
@Column(name = "archive_checksum")
private String archiveChecksum;
@Column(name = "retention_period_months", nullable = false)
private Integer retentionPeriodMonths;
@Column(name = "archive_status", nullable = false)
@Enumerated(EnumType.STRING)
private ArchiveStatus archiveStatus;
@CreatedDate
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
@CreatedBy
@Column(name = "created_by", nullable = false)
private String createdBy;
// 构造方法、getter和setter省略
}
public enum ArchiveStatus {
PENDING, IN_PROGRESS, COMPLETED, FAILED, VERIFIED
}
@Service
public class AuditArchiveService {
@Autowired
private AuditLogRepository auditLogRepository;
@Autowired
private AuditArchiveRepository auditArchiveRepository;
@Value("${audit.archive.retention-months:12}")
private int defaultRetentionMonths;
@Value("${audit.archive.batch-size:1000}")
private int batchSize;
@Scheduled(cron = "0 2 * * * *") // 每天凌晨2点执行
public void scheduleAuditArchiving() {
LocalDateTime archiveCutoff = LocalDateTime.now().minusMonths(defaultRetentionMonths);
archiveOldAuditLogs(archiveCutoff);
}
@Transactional
public void archiveOldAuditLogs(LocalDateTime cutoffDate) {
List<AuditLog> logsToArchive = auditLogRepository.findByOperationTimestampBefore(cutoffDate);
if (logsToArchive.isEmpty()) {
return;
}
AuditArchive archive = new AuditArchive();
archive.setArchiveDate(LocalDate.now());
archive.setArchivedRecordsCount((long) logsToArchive.size());
archive.setRetentionPeriodMonths(defaultRetentionMonths);
archive.setArchiveStatus(ArchiveStatus.IN_PROGRESS);
archive = auditArchiveRepository.save(archive);
try {
String archiveFilePath = createArchiveFile(logsToArchive, archive.getId());
String checksum = calculateFileChecksum(archiveFilePath);
archive.setArchiveFilePath(archiveFilePath);
archive.setArchiveChecksum(checksum);
archive.setArchiveStatus(ArchiveStatus.COMPLETED);
// 分批删除已归档的记录以避免锁表
deleteArchivedRecordsInBatches(logsToArchive.stream()
.map(AuditLog::getId)
.collect(Collectors.toList()));
} catch (Exception e) {
archive.setArchiveStatus(ArchiveStatus.FAILED);
throw new RuntimeException("Failed to archive audit logs", e);
} finally {
auditArchiveRepository.save(archive);
}
}
private String createArchiveFile(List<AuditLog> auditLogs, Long archiveId) throws IOException {
String fileName = String.format("audit_archive_%d_%s.json",
archiveId, LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
String filePath = "/audit/archives/" + fileName;
// 创建目录如果不存在
Path archiveDir = Paths.get("/audit/archives/");
Files.createDirectories(archiveDir);
// 序列化审计日志为JSON格式
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
try (FileWriter writer = new FileWriter(filePath)) {
objectMapper.writeValue(writer, auditLogs);
}
return filePath;
}
private String calculateFileChecksum(String filePath) throws IOException, NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (FileInputStream fis = new FileInputStream(filePath);
DigestInputStream dis = new DigestInputStream(fis, digest)) {
byte[] buffer = new byte[8192];
while (dis.read(buffer) != -1) {
// 读取文件内容计算摘要
}
}
return Base64.getEncoder().encodeToString(digest.digest());
}
private void deleteArchivedRecordsInBatches(List<Long> auditLogIds) {
for (int i = 0; i < auditLogIds.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, auditLogIds.size());
List<Long> batchIds = auditLogIds.subList(i, endIndex);
auditLogRepository.deleteByIdIn(batchIds);
}
}
public List<AuditLog> retrieveArchivedLogs(Long archiveId) throws IOException {
AuditArchive archive = auditArchiveRepository.findById(archiveId)
.orElseThrow(() -> new EntityNotFoundException("Archive not found"));
if (archive.getArchiveStatus() != ArchiveStatus.COMPLETED) {
throw new IllegalStateException("Archive is not completed");
}
// 验证文件完整性
String currentChecksum = calculateFileChecksum(archive.getArchiveFilePath());
if (!currentChecksum.equals(archive.getArchiveChecksum())) {
throw new RuntimeException("Archive file integrity check failed");
}
// 从归档文件读取数据
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
return objectMapper.readValue(
new File(archive.getArchiveFilePath()),
new TypeReference<List<AuditLog>>() {}
);
}
public AuditPerformanceMetrics getPerformanceMetrics() {
long totalActiveRecords = auditLogRepository.count();
long totalArchivedRecords = auditArchiveRepository.sumArchivedRecordsCount();
// 计算最近查询的平均响应时间
long averageQueryTime = measureAverageQueryTime();
// 计算存储空间使用情况
long estimatedStorageSize = estimateStorageSize(totalActiveRecords);
return new AuditPerformanceMetrics(
totalActiveRecords,
totalArchivedRecords,
averageQueryTime,
estimatedStorageSize,
calculateOptimizationRecommendations(totalActiveRecords, averageQueryTime)
);
}
private long measureAverageQueryTime() {
// 执行几个典型查询并测量平均时间
long totalTime = 0;
int queryCount = 5;
for (int i = 0; i < queryCount; i++) {
long startTime = System.currentTimeMillis();
auditLogRepository.findByOperationTimestampAfter(LocalDateTime.now().minusDays(7));
totalTime += (System.currentTimeMillis() - startTime);
}
return totalTime / queryCount;
}
private long estimateStorageSize(long recordCount) {
// 估算每条记录的平均大小(字节)
long averageRecordSize = 2048; // 2KB per record estimate
return recordCount * averageRecordSize;
}
private List<String> calculateOptimizationRecommendations(long recordCount, long averageQueryTime) {
List<String> recommendations = new ArrayList<>();
if (recordCount > 1000000) {
recommendations.add("考虑增加索引以提升查询性能");
recommendations.add("建议更频繁地执行归档操作");
}
if (averageQueryTime > 5000) {
recommendations.add("查询响应时间较长,建议优化查询条件");
recommendations.add("考虑使用分表策略");
}
return recommendations;
}
}
@Repository
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
List<AuditLog> findByOperationTimestampBefore(LocalDateTime cutoffDate);
@Modifying
void deleteByIdIn(List<Long> ids);
// 其他查询方法保持不变
}
@Repository
public interface AuditArchiveRepository extends JpaRepository<AuditArchive, Long> {
@Query("SELECT SUM(a.archivedRecordsCount) FROM AuditArchive a WHERE a.archiveStatus = 'COMPLETED'")
Long sumArchivedRecordsCount();
List<AuditArchive> findByArchiveStatusAndCreatedAtBefore(ArchiveStatus status, LocalDateTime before);
}
// 性能指标数据传输对象
public class AuditPerformanceMetrics {
private long totalActiveRecords;
private long totalArchivedRecords;
private long averageQueryTimeMs;
private long estimatedStorageSizeBytes;
private List<String> optimizationRecommendations;
// 构造方法、getter和setter省略
}
Spring Data JPA提供的审计注解为Java企业级应用构建了完整的数据变更追踪体系。通过@CreatedDate、@LastModifiedDate、@CreatedBy和@LastModifiedBy等注解的合理配置和使用,开发者能够实现自动化的审计功能,显著提升数据治理的效率和准确性。审计功能的价值不仅体现在满足合规要求上,更重要的是为业务决策和系统优化提供了宝贵的数据支持。在复杂的企业环境中,审计系统需要支持多层级权限控制、数据归档管理和性能优化等高级特性,这些功能的实现需要结合业务需求和技术架构进行精心设计。