Mastering Elegant Operation Log Design with AOP and SpEL in Java
This article explains how to implement clean, decoupled operation logs in Java applications using AOP, SpEL, custom annotations, and dynamic templates, covering use cases, implementation methods, code examples, parsing logic, context handling, and integration with Spring Boot.
1. Operation Log Use Cases
Operation logs exist in almost every system and must be simple and understandable, unlike system logs which are mainly for debugging.
2. Implementation Methods
2.1 Using Canal to Listen to Database Changes
Canal parses MySQL binlog to capture data changes and record operation logs, fully separating logs from business logic. However, it cannot capture RPC calls.
2.2 Recording Logs via Log Files
log.info("Order created");
log.info("Order created, orderNo:{}", orderNo);
log.info("Modified address: from \"{}\" to \"{}\"", "Gold Community", "Silver Community");Three problems need solving:
How to record the operator who modifies the address.
How to bind the log to the specific order.
How to obtain the old address without adding a method parameter.
2.3 Using LogUtil
LogUtil.log(orderNo, "Order created", "Xiao Ming");
LogUtil.log(orderNo, "Order created, orderNo" + orderNo, "Xiao Ming");
String template = "User %s modified address from \"%s\" to \"%s\"";
LogUtil.log(orderNo, String.format(template, "Xiao Ming", "Gold", "Silver"), "Xiao Ming");2.4 Method Annotation for Operation Logs
@LogRecord(content = "Modified delivery address")
public void modifyAddress(UpdateDeliveryRequest request) {
// update logic
}Static annotation text is not dynamic; to include variables we need SpEL.
3. Elegant AOP Support for Dynamic Operation Logs
3.1 Dynamic Templates
Using SpEL inside annotation content allows placeholders to be replaced at runtime.
@LogRecord(content = "Modified address: from \"#oldAddress\", to \"#request.address\"")
public void modifyAddress(UpdateDeliveryRequest request, String oldAddress) { ... }Problems solved by adding operator and bizNo parameters, and using a user context to obtain the current user.
4. Code Implementation Details
4.1 Code Structure
The operation log is implemented via an AOP interceptor, consisting of AOP module, parsing module, persistence module, and starter module.
4.2 Parsing Logic
SpEL is used to evaluate expressions. The LogRecordExpressionEvaluator caches parsed expressions.
public String parseExpression(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
return getExpression(this.expressionCache, methodKey, conditionExpression)
.getValue(evalContext, String.class);
}The LogRecordEvaluationContext adds method parameters, context variables, return value, and error message to the SpEL root object.
public LogRecordEvaluationContext(Object rootObject, Method method, Object[] arguments,
ParameterNameDiscoverer parameterNameDiscoverer, Object ret, String errorMsg) {
super(rootObject, method, arguments, parameterNameDiscoverer);
Map<String, Object> variables = LogRecordContext.getVariables();
if (variables != null && !variables.isEmpty()) {
for (Map.Entry<String, Object> entry : variables.entrySet()) {
setVariable(entry.getKey(), entry.getValue());
}
}
setVariable("_ret", ret);
setVariable("_errorMsg", errorMsg);
} LogRecordContextuses an
InheritableThreadLocal<Stack<Map<String,Object>>>to store variables per method call, avoiding interference between nested annotated methods.
4.3 Default Operator Logic
The IOperatorGetService interface provides the current user. If the annotation does not specify an operator, the interceptor retrieves it from this service.
4.4 Custom Function Logic
Custom functions implement IParseFunction. The ParseFunctionFactory registers them, and DefaultFunctionServiceImpl invokes apply to transform values.
4.5 Log Persistence Logic
The ILogRecordService interface defines record(LogRecord logRecord). Users can implement it to store logs in files, databases, Elasticsearch, etc.
@Slf4j
public class DefaultLogRecordServiceImpl implements ILogRecordService {
@Override
public void record(LogRecord logRecord) {
log.info("【logRecord】log={}", logRecord);
}
}4.6 Starter Auto‑Configuration
Adding @EnableLogRecord(tenant = "com.mzt.test") imports LogRecordConfigureSelector, which registers beans such as LogRecordOperationSource, LogRecordInterceptor, default implementations of IOperatorGetService, ILogRecordService, and the AOP advisor.
5. Summary
The article presented common approaches to operation logging and demonstrated how to build a flexible, decoupled logging component using AOP, SpEL, custom annotations, and dynamic templates, providing a clear path for developers to implement readable and maintainable operation logs.
7. References
Canal
Spring Framework
Spring Expression Language (SpEL)
ThreadLocal, InheritableThreadLocal, TransmittableThreadLocal differences
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
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.
