Mastering Spring Boot 3 Auditing: AOP, SpEL, Async & Batch Logging
This article introduces a Spring Boot 3 case collection with over 100 permanent examples and then provides a complete tutorial on implementing audit logging using Spring AOP, SpEL, asynchronous processing, custom annotations, JPA entities, and batch persistence to efficiently record operation details.
1. Introduction
The Spring Boot 3 practical case collection now includes more than 100 carefully selected articles, promises permanent updates for subscribers, and offers the complete source code and MD documentation to ensure smooth learning.
2. Practical Case – Audit Logging
2.1 Custom Annotation
<code>@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OperationAudit {
/**操作类型编码*/
String operationCode();
/**操作描述(支持SpEL表达式)*/
String description() default "";
/**操作人表达式*/
String operator() default "@auditOperator.getName";
/**前置状态提取表达式(操作前)*/
String preState() default "";
/**后置状态提取表达式(操作后)*/
String postState() default "";
}</code>2.2 Audit Entity and Repository
<code>@Entity
@Table(name = "sys_audit_log")
public class AuditRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String operationCode;
@Column(length = 500)
private String description;
private String operator;
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime operationTime;
@Lob @Basic(fetch = FetchType.LAZY)
private String preState;
@Lob @Basic(fetch = FetchType.LAZY)
private String postState;
private boolean success;
private String errorMessage;
// getters, setters
}
</code> <code>public interface AuditRepository extends JpaRepository<AuditRecord, Long> { }
</code>2.3 Asynchronous Recorder
<code>@Component
public class AuditRecorder {
private volatile long lastFlushTime = System.currentTimeMillis();
private static final long MAX_FLUSH_INTERVAL = 3000;
private final BlockingQueue<AuditRecord> auditQueue = new LinkedBlockingQueue<>(1000);
private final Executor executor = Executors.newSingleThreadExecutor();
private final AuditRepository auditRepository;
public AuditRecorder(AuditRepository auditRepository) {
this.auditRepository = auditRepository;
startConsumer();
}
public void record(AuditRecord record) {
if (!auditQueue.offer(record)) {
System.err.printf("Audit queue is full, record will be discarded: %s%n", record);
}
}
private void startConsumer() {
executor.execute(() -> {
List<AuditRecord> buffer = new ArrayList<>(100);
while (!Thread.interrupted()) {
try {
AuditRecord record = auditQueue.poll(MAX_FLUSH_INTERVAL, TimeUnit.MILLISECONDS);
if (record != null) {
buffer.add(record);
}
boolean reachBatchSize = buffer.size() >= 100;
boolean reachTimeLimit = System.currentTimeMillis() - lastFlushTime > MAX_FLUSH_INTERVAL;
if (reachBatchSize || reachTimeLimit) {
if (!buffer.isEmpty()) {
auditRepository.saveAll(buffer);
buffer.clear();
lastFlushTime = System.currentTimeMillis();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (!buffer.isEmpty()) {
auditRepository.saveAll(buffer);
}
});
}
}
</code>2.4 Audit Aspect
<code>@Aspect
@Component
public class AuditAspect implements BeanFactoryAware {
private static final Logger logger = LoggerFactory.getLogger(AuditAspect.class);
private BeanFactory beanFactory;
private final AuditRecorder auditRecorder;
private final ExpressionParser parser = new SpelExpressionParser();
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
public AuditAspect(AuditRecorder auditRecorder) {
this.auditRecorder = auditRecorder;
}
@Around("@annotation(auditAnnotation)")
public Object auditOperation(ProceedingJoinPoint joinPoint, OperationAudit auditAnnotation) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Object[] args = joinPoint.getArgs();
EvaluationContext context = createEvaluationContext(method, args);
AuditRecord record = prepareAuditRecord(auditAnnotation, context);
record.setPreState(evaluateExpression(auditAnnotation.preState(), context));
try {
Object result = joinPoint.proceed();
context.setVariable("result", result);
record.setPostState(evaluateExpression(auditAnnotation.postState(), context));
record.setSuccess(true);
return result;
} catch (Exception ex) {
record.setSuccess(false);
record.setErrorMessage(ex.getMessage());
throw ex;
} finally {
auditRecorder.record(record);
}
}
private EvaluationContext createEvaluationContext(Method method, Object[] args) {
MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(null, method, args, parameterNameDiscoverer);
context.setBeanResolver(new BeanFactoryResolver(this.beanFactory));
return context;
}
private AuditRecord prepareAuditRecord(OperationAudit annotation, EvaluationContext context) {
AuditRecord record = new AuditRecord();
record.setOperationCode(annotation.operationCode());
record.setDescription(evaluateExpression(annotation.description(), context));
record.setOperator(evaluateExpression(annotation.operator(), context));
record.setOperationTime(LocalDateTime.now());
return record;
}
private String evaluateExpression(String expr, EvaluationContext context) {
try {
return parser.parseExpression(expr).getValue(context, String.class);
} catch (Exception e) {
logger.warn("SpEL evaluation failed: {}", expr, e);
return "";
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
}
</code>2.5 Test Example
<code>@Service
public class OrderService {
@OperationAudit(
operationCode = "ORDER_STATUS_UPDATE",
description = "'更新订单状态,订单号, ' + #orderNo",
preState = "#oldStatus",
postState = "#newStatus")
public void updateOrderStatus(String orderNo, Integer oldStatus, Integer newStatus) {
System.err.printf("更新订单: %s; 从状态: %s, 到状态: %s%n", orderNo, oldStatus, newStatus);
}
}
</code> <code>@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@GetMapping("/update/{orderNo}/{oldStatus}/{newStatus}")
public ResponseEntity<String> updateStatus(@PathVariable("orderNo") String orderNo,
@PathVariable Integer oldStatus,
@PathVariable Integer newStatus) {
orderService.updateOrderStatus(orderNo, oldStatus, newStatus);
return ResponseEntity.ok("success");
}
}
</code>Test results are displayed in the following screenshots.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.