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.

21CTO
21CTO
21CTO
Mastering Elegant Operation Log Design with AOP and SpEL in Java

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

uses 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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaaopspringSpELloggingOperation Log
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.